heat-dashboard-3.0.1/0000775000175000017500000000000013737341153014412 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/0000775000175000017500000000000013737341153017103 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/notes/0000775000175000017500000000000013737341153020233 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/notes/drop-py-2-7-3dafc6e9e6f29bda.yaml0000664000175000017500000000033213737340771025544 0ustar zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. Last release of heat-dashboard to support Python 2.7 is OpenStack Train. The minimum version of Python now supported by heat-dashboard is Python 3.6. heat-dashboard-3.0.1/releasenotes/notes/enabled_file_for_tmplgen-71432805cbfe4176.yaml0000664000175000017500000000142513737340771030170 0ustar zuulzuul00000000000000--- upgrade: - | If you need to update your queens heat-dashboard to rocky, you must copy the latest enabled file (_1650_project_template_generator_panel.py) to horizon/openstack_dashboard/enabled directory again. The enabled file for template generator has been changed a lot in rocky and it does not have compatibility with queens heat-dashboard. The background of this change is that SCSS and xstatic modules are started to be used rather than CSS and embedded JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 `__] [`bug 1755140 `__] [`bug 1755308 `__]heat-dashboard-3.0.1/releasenotes/notes/split-out-from-horizon-1d1eabed58eb885e.yaml0000664000175000017500000000070713737340771030161 0ustar zuulzuul00000000000000--- prelude: > Heat support in OpenStack Dashboard is now split into a separated python package. features: - | Heat support in OpenStack Dashboard is now split into a separete package ``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading OpenStack Dashboard to Queens release and add ``enabled`` file for Heat Dashboard. For detail information, see https://docs.openstack.org/heat-dashboard/latest/. heat-dashboard-3.0.1/releasenotes/notes/template-generator-panel-decc3ec172bfa1dd.yaml0000664000175000017500000000041013737340771030576 0ustar zuulzuul00000000000000--- features: - | A panel for ``Template Generator`` is newly added. There is not specific installation process. After installing heat-dashboard, this panel will be displayed along with ``Stacks``, ``Resource Types`` and ``Template Versions``. heat-dashboard-3.0.1/releasenotes/notes/.placeholder0000664000175000017500000000000013737340771022511 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/0000775000175000017500000000000013737341153020403 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/_templates/0000775000175000017500000000000013737341153022540 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/_templates/.placeholder0000664000175000017500000000000013737340771025016 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/index.rst0000664000175000017500000000025113737340772022250 0ustar zuulzuul00000000000000============================ Heat Dashboard Release Notes ============================ .. toctree:: :maxdepth: 1 unreleased train stein rocky queens heat-dashboard-3.0.1/releasenotes/source/train.rst0000664000175000017500000000017613737340771022263 0ustar zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train heat-dashboard-3.0.1/releasenotes/source/queens.rst0000664000175000017500000000022313737340771022437 0ustar zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens heat-dashboard-3.0.1/releasenotes/source/unreleased.rst0000664000175000017500000000016013737340771023266 0ustar zuulzuul00000000000000============================== Current Series Release Notes ============================== .. release-notes:: heat-dashboard-3.0.1/releasenotes/source/rocky.rst0000664000175000017500000000022113737340771022264 0ustar zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky heat-dashboard-3.0.1/releasenotes/source/_static/0000775000175000017500000000000013737341153022031 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/_static/.placeholder0000664000175000017500000000000013737340771024307 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/0000775000175000017500000000000013737341153021642 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/en_GB/0000775000175000017500000000000013737341153022614 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000013737341153024401 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000001071413737340772027443 0ustar zuulzuul00000000000000# Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata # Andi Chandler , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-12-23 14:09+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-12-21 01:11+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgid "Current Series Release Notes" msgstr "Current Series Release Notes" msgid "Heat Dashboard Release Notes" msgstr "Heat Dashboard Release Notes" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "Heat support in OpenStack Dashboard is now split into a separate package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "If you need to update your Queens Heat-dashboard to Rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to Horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in Rocky and it does not have compatibility " "with Queens Heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgid "New Features" msgstr "New Features" msgid "Prelude" msgstr "Prelude" msgid "" "Python 2.7 support has been dropped. Last release of heat-dashboard to " "support Python 2.7 is OpenStack Train. The minimum version of Python now " "supported by heat-dashboard is Python 3.6." msgstr "" "Python 2.7 support has been dropped. Last release of heat-dashboard to " "support Python 2.7 is OpenStack Train. The minimum version of Python now " "supported by heat-dashboard is Python 3.6." msgid "Queens Series Release Notes" msgstr "Queens Series Release Notes" msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Stein Series Release Notes" msgstr "Stein Series Release Notes" msgid "Train Series Release Notes" msgstr "Train Series Release Notes" msgid "Upgrade Notes" msgstr "Upgrade Notes" heat-dashboard-3.0.1/releasenotes/source/locale/pt_BR/0000775000175000017500000000000013737341153022650 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/pt_BR/LC_MESSAGES/0000775000175000017500000000000013737341153024435 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/pt_BR/LC_MESSAGES/releasenotes.po0000664000175000017500000000777513737340771027513 0ustar zuulzuul00000000000000# Fernando Pimenta , 2018. #zanata # Rodrigo Loures , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-03-15 15:30+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-04-24 07:19+0000\n" "Last-Translator: Fernando Pimenta \n" "Language-Team: Portuguese (Brazil)\n" "Language: pt_BR\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "Um painel para ``Template Generator`` está recentemente adicionado. Não há " "um processo de instalação específico. Depois de instalar heat-dashboard, " "esse painel será exibido juntamente com ``Stacks``, ``Resource Types`` e " "``Template Versions``." msgid "Current Series Release Notes" msgstr "Notas de Versão da Série Atual" msgid "Heat Dashboard Release Notes" msgstr "Heat Dashboard - Notas de Versão" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "O suporte do Heat no dashboard do Openstack agora está dividido em um " "pacote de Pyton separado." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "O suporte do Heat no dashboard do Openstack agora está dividido em um pacote " "de Pyton separado ``heat-dashboard``. Você precisa instalar ``heat-" "dashboard`` depois atualizar o dashboard do OpenStack para a versão Queens e " "adicione ``enabled`` arquivo para o dashboard do Heat. Para mais informações " "acesse https://docs.openstack.org/heat-dashboard/latest/." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "Se você precisar atualizar o seu dashboard heat do queens para rocky, você " "deve copiar o último arquivo habilitado " "(_1650_project_template_generator_panel.py) para o diretório horizon/" "openstack_dashboard/enabled novamente. O arquivo habilitado para gerador de " "modelos foi muito modificado no rocky e não tem compatibilidade com o " "dashboard heat do queens. O que está por trás desta alteração é que os " "módulos SCSS e xtatic começaram a ser utilizados em vez do CSS e das cópias " "embutidas de JavaScript para seguir a regra de contribuição do horizon. " "[`bug 1753919 `__] " "[`bug 1755140 `__] " "[`bug 1755308 `__]" msgid "New Features" msgstr "Novos Recursos" msgid "Prelude" msgstr "Prelúdio" msgid "Queens Series Release Notes" msgstr "Notas de Versão da Série Queens" msgid "Upgrade Notes" msgstr "Notas de Atualização" heat-dashboard-3.0.1/releasenotes/source/locale/ko_KR/0000775000175000017500000000000013737341153022647 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ko_KR/LC_MESSAGES/0000775000175000017500000000000013737341153024434 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ko_KR/LC_MESSAGES/releasenotes.po0000664000175000017500000001035713737340771027500 0ustar zuulzuul00000000000000# ByungYeol Woo , 2017. #zanata # Sungjin Kang , 2017. #zanata # johjuhyun , 2017. #zanata # minwook-shin , 2017. #zanata # ByungYeol Woo , 2018. #zanata # Ian Y. Choi , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-03-15 15:30+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-04-25 10:34+0000\n" "Last-Translator: ByungYeol Woo \n" "Language-Team: Korean (South Korea)\n" "Language: ko_KR\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=1; plural=0\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "``Template Generator``를 위한 패널이 새롭게 추가되었습니다. 특정한 설치 절차" "가 없습니다. heat 대시보드가 설치된 다음, 이 패널은 ``Stacks``, ``Resource " "Types``, ``Template Versions``에 따라서 표시됩니다." msgid "Current Series Release Notes" msgstr "최신 시리즈에 대한 릴리즈 노트" msgid "Heat Dashboard Release Notes" msgstr "Heat 대시보드 릴리즈 노트" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "오픈스택 대시보드에서 지원하는 Heat는 현재 별도의 파이썬 패키지로 나누어져 있" "습니다." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "오픈스택 대시보드에서 지원하는 Heat는 현재 ``heat-dashboard``라는 별개의 패키" "지로 나뉘어 있습니다. 오픈스택 대시보드를 Queens 릴리즈로 업그레이드한 다음 " "``heat-dashboard``를 설치하고 Head 대시보드에 ``enabled`` 파일을 추가할 필요" "가 있습니다. 자세한 사항은 https://docs.openstack.org/heat-dashboard/" "latest/ 를 참조하세요." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "Queens heat-dashboard를 Rocky로 업데이트해야 하는 경우 최신 활성화 파일 " "(_1650_project_template_generator_panel.py)을 horizon/openstack_dashboard/" "enabled 디렉토리로 다시 복사해야 합니다. 템플릿 생성기에 사용할 수 있는 파일" "이 많이 변경되어 Queens heat-dashboard와 호환되지 않습니다. Horizon의 기여 규" "칙을 따르기 위해 SCSS 및 xstatic 모듈이 CSS 및 내장된 JavaScript 복사본 대신 " "사용되기 시작한 것이 이 변경 사항이 나오게 된 배경에 해당합니다. [`bug " "1753919 `__] [`bug " "1755140 `__] [`bug " "1755308 `__]" msgid "New Features" msgstr "새로운 기능" msgid "Prelude" msgstr "서문" msgid "Queens Series Release Notes" msgstr "Queens 시리즈 릴리즈 노트" msgid "Upgrade Notes" msgstr "업그레이드 노트" heat-dashboard-3.0.1/releasenotes/source/locale/de/0000775000175000017500000000000013737341153022232 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/de/LC_MESSAGES/0000775000175000017500000000000013737341153024017 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/de/LC_MESSAGES/releasenotes.po0000664000175000017500000001065213737340772027062 0ustar zuulzuul00000000000000# Andreas Jaeger , 2017. #zanata # Frank Kloeker , 2017. #zanata # Robert Simai , 2017. #zanata # Reik Keutterling , 2018. #zanata # Robert Simai , 2018. #zanata # Andreas Jaeger , 2019. #zanata # Robert Simai , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-09-27 21:37+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-09-28 07:30+0000\n" "Last-Translator: Andreas Jaeger \n" "Language-Team: German\n" "Language: de\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "Ein Panel für `` Vorlagengenerator`` wurde neu hinzugefügt. Es gibt keinen " "spezifischen Installationsvorgang. Nach der Installation von heat-dashboard " "wird dieses Panel zusammen mit `` Stacks``, `` Ressourcentypen`` und `` " "Vorlagenversionen`` angezeigt." msgid "Current Series Release Notes" msgstr "Aktuelle Serie Releasenotes" msgid "Heat Dashboard Release Notes" msgstr "Heat Dashboard Releasenotes" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "Die Heat-Unterstützung im OpenStack Dashboard ist jetzt in ein separates " "Python-Paket aufgeteilt." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "Die Heat-Unterstützung im OpenStack Dashboard ist jetzt in ein separates " "Python-Paket aufgeteilt. ``heat-dashboard``. Sie müssen `` heat-dashboard`` " "installieren, nachdem Sie das OpenStack Dashboard auf Queens aktualisiert " "haben und die ``enabled`` Datei für das Heat Dashboard hinzufügen. Weitere " "Informationen finden Sie unter https://docs.openstack.org/heat-dashboard/" "latest/." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "Wenn Sie das Queens heat-dashboard nach Rocky aktualisieren müssen Sie die " "aktuellste enabled-Datei (_1650_project_template_generator_panel.py) wieder " "in das horizon/openstack_dashboard/enabled Verzeichnis laden. Die enabled-" "Datei für den Vorlagengenerator hat sich in Rocky stark geändert und ist " "nicht kompatibel mit dem Queens heat-dashboard. Hintergrund der Änderung " "ist, dass begonnen wird, SCSS und xstatic Module zu verwenden, zum Nachteil " "von CSS und embedded JavaScript, um den Regeln für die Horizon " "Zusammenarbeit zu genügen. [`bug 1753919 `__] [`bug 1755140 `__] [`bug 1755308 `__]" msgid "New Features" msgstr "Neue Funktionen" msgid "Prelude" msgstr "Einleitung" msgid "Queens Series Release Notes" msgstr "Versionshinweise Queens" msgid "Rocky Series Release Notes" msgstr "Rocky Serie Releasenotes" msgid "Stein Series Release Notes" msgstr "Stein Serie Releasenotes" msgid "Train Series Release Notes" msgstr "Train Serie Releasenotes" msgid "Upgrade Notes" msgstr "Aktualisierungsnotizen" heat-dashboard-3.0.1/releasenotes/source/locale/ne/0000775000175000017500000000000013737341153022244 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ne/LC_MESSAGES/0000775000175000017500000000000013737341153024031 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ne/LC_MESSAGES/releasenotes.po0000664000175000017500000001030213737340771027063 0ustar zuulzuul00000000000000# Surit Aryal , 2018. #zanata # Surit Aryal , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-22 15:39+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-06-17 09:37+0000\n" "Last-Translator: Surit Aryal \n" "Language-Team: Nepali\n" "Language: ne\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "1.0.0" msgstr "१.०.०" msgid "1.1.0" msgstr "१.१.०" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgid "Current Series Release Notes" msgstr "हालको सीरीज रिलीज नोट्स" msgid "Heat Dashboard Release Notes" msgstr "Heat ड्यासबोर्ड रिलीज नोटहरू" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgid "New Features" msgstr "नयाँ सुविधाहरू" msgid "Prelude" msgstr "सुरुवाती" msgid "Queens Series Release Notes" msgstr "Queens श्रृंखला जारी नोटहरू" msgid "Rocky Series Release Notes" msgstr "Rocky श्रृंखला जारी नोटहरू" msgid "Stein Series Release Notes" msgstr "Stein श्रृंखला जारी नोटहरू" msgid "Upgrade Notes" msgstr "अपग्रेड टिप्पणीहरू" heat-dashboard-3.0.1/releasenotes/source/locale/zh_CN/0000775000175000017500000000000013737341153022643 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/zh_CN/LC_MESSAGES/0000775000175000017500000000000013737341153024430 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/zh_CN/LC_MESSAGES/releasenotes.po0000664000175000017500000000767113737340771027501 0ustar zuulzuul00000000000000# Bin , 2017. #zanata # TigerFang , 2017. #zanata # Tony , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-09-02 11:20+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-09-23 02:33+0000\n" "Last-Translator: Tony \n" "Language-Team: Chinese (China)\n" "Language: zh_CN\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=1; plural=0\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "新添加了一个 ``Template Generator`` 面板。它并不是只针对安装的过程。在安装了 " "heat-dashboard 后,这个面板会和 ``Stacks``、``Resource Types`` 和 ``Template " "Versions`` 一起显示。" msgid "Current Series Release Notes" msgstr "当前版本发布声明" msgid "Heat Dashboard Release Notes" msgstr "Heat Dashboard 发行注记" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "在 OpenStack Dashboard 中对 Heat 的支持现在被分离到一个独立的 python 软件包" "中。" msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "在 OpenStack Dashboard 中对 Heat 的支持现在被分离到一个名为 ``heat-" "dashboard`` 的独立软件包中。在把 OpenStack Dashboard 升级到 Queens 版本后,需" "要安装 ``heat-dashboard``,并为 Heat Dashboard 添加 ``enabled`` 文件。如需了" "解相关的详细信息,请参阅 https://docs.openstack.org/heat-dashboard/latest/。" msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "如果需要把 queens heat-dashboard 升级到 rocky,则需要把最新的启用文件 " "(_1650_project_template_generator_panel.py) 再次复制到 horizon/" "openstack_dashboard/enabled 目录中。在 rocky 版本中,template generator 的启" "用文件有了很大的变化,它与 queens 版本的 heat-dashboard 不兼容。这个变化的背" "景是,为了遵循 Horizon 的贡献规则,开始使用 SCSS 和 xstatic 模块来替代 CSS " "和内嵌的 JavaScript 文件。 [`bug 1753919 `__] [`bug 1755140 `__] [`bug 1755308 `__]" msgid "New Features" msgstr "新功能" msgid "Prelude" msgstr "前奏" msgid "Queens Series Release Notes" msgstr "Queens 系列发行注记" msgid "Rocky Series Release Notes" msgstr "Rocky 系列发行注记" msgid "Stein Series Release Notes" msgstr "Stein版本发行注记" msgid "Upgrade Notes" msgstr "升级注记" heat-dashboard-3.0.1/releasenotes/source/locale/id/0000775000175000017500000000000013737341153022236 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/id/LC_MESSAGES/0000775000175000017500000000000013737341153024023 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/id/LC_MESSAGES/releasenotes.po0000664000175000017500000001002413737340771027056 0ustar zuulzuul00000000000000# suhartono , 2017. #zanata # suhartono , 2018. #zanata # suhartono , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-07-08 10:08+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-07-16 06:35+0000\n" "Last-Translator: suhartono \n" "Language-Team: Indonesian\n" "Language: id\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=1; plural=0\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "Panel untuk ``Template Generator`` baru saja ditambahkan. Tidak ada proses " "instalasi yang spesifik. Setelah memasang heat-dashboard, panel ini akan " "ditampilkan bersama dengan ``Stacks``, ``Resource Types`` dan ``Template " "Versions``." msgid "Current Series Release Notes" msgstr "Catatan rilis seri saat ini" msgid "Heat Dashboard Release Notes" msgstr "Catatan Rilis Heat Dashboard" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "Dukungan Heat di OpenStack Dashboard kini terbagi menjadi paket python " "terpisah." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "Dukungan Heat di OpenStack Dashboard sekarang terbagi menjadi paket terpisah " "``heat-dashboard``. Anda perlu menginstal ``heat-dashboard`` setelah " "mengupgrade Dashboard OpenStack ke rilis Queens dan menambahkan file " "``enabled`` untuk Heat Dashboard. Untuk informasi lebih detail, lihat " "https://docs.openstack.org/heat-dashboard/latest/." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "Jika Anda perlu memperbarui queens heat-dashboard Anda ke rocky, Anda harus " "menyalin file terbaru yang diaktifkan " "(_1650_project_template_generator_panel.py) ke direktori horizon/" "openstack_dashboard/enabled lagi. File yang diaktifkan untuk generator " "template telah banyak berubah dalam rocky dan tidak memiliki kompatibilitas " "dengan queens heat-dashboard. Latar belakang perubahan ini adalah bahwa SCSS " "dan modul xstatic mulai digunakan daripada CSS dan salinan embedded " "JavaScript untuk mengikuti aturan kontribusi Horizon. [`bug 1753919 ` __] [`bug 1755140 `__] [` bug 1755308 " " `__]" msgid "New Features" msgstr "Fitur baru" msgid "Prelude" msgstr "Prelude" msgid "Queens Series Release Notes" msgstr "Catatan Rilis Seri Queens" msgid "Rocky Series Release Notes" msgstr "Catatan Rilis Seri Rocky" msgid "Stein Series Release Notes" msgstr "Catatan Rilis Seri Stein" msgid "Upgrade Notes" msgstr "Catatan Upgrade" heat-dashboard-3.0.1/releasenotes/source/locale/ja/0000775000175000017500000000000013737341153022234 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ja/LC_MESSAGES/0000775000175000017500000000000013737341153024021 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ja/LC_MESSAGES/releasenotes.po0000664000175000017500000000473413737340771027067 0ustar zuulzuul00000000000000# Yuko Katabami , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: Heat Dashboard Release Notes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-02-15 01:11+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-02-14 10:01+0000\n" "Last-Translator: Yuko Katabami \n" "Language-Team: Japanese\n" "Language: ja\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=1; plural=0\n" msgid "1.0.0" msgstr "1.0.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "「テンプレートジェネレーター」用のパネルが新たに追加されました。特定のインス" "トールプロセスはありません。heat-dashboard のインストール後には、「スタック」" "「リソース種別」「テンプレートバージョン」とともに表示されます。" msgid "Current Series Release Notes" msgstr "開発中バージョンのリリースノート" msgid "Heat Dashboard Release Notes" msgstr "Heat Dashboard リリースノート" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "OpenStack Dashboard における Heat のサポートは、別の python パッケージに分離" "されました。" msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "OpenStack Dashboard における Heat のサポートは、「heat-dashboard」という別の" "パッケージに分離されました。OpenStack Dashboard を Queens リリースにアップグ" "レードした後に「heat-dashboard」をインストールして、Heat Dashboard を" "「enabled」ファイルを追加する必要があります。詳細については、https://docs." "openstack.org/heat-dashboard/latest/ を参照してください。" msgid "New Features" msgstr "新機能" msgid "Prelude" msgstr "前置き" msgid "Queens Series Release Notes" msgstr "Queens バージョンのリリースノート" heat-dashboard-3.0.1/releasenotes/source/locale/ru/0000775000175000017500000000000013737341153022270 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ru/LC_MESSAGES/0000775000175000017500000000000013737341153024055 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/releasenotes/source/locale/ru/LC_MESSAGES/releasenotes.po0000664000175000017500000001211413737340772027113 0ustar zuulzuul00000000000000# Artem , 2017. #zanata # Roman Gorshunov , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: heat-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-08-05 13:28+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-08-05 09:49+0000\n" "Last-Translator: Roman Gorshunov \n" "Language-Team: Russian\n" "Language: ru\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" msgid "1.0.0" msgstr "1.0.0" msgid "1.1.0" msgstr "1.1.0" msgid "" "A panel for ``Template Generator`` is newly added. There is not specific " "installation process. After installing heat-dashboard, this panel will be " "displayed along with ``Stacks``, ``Resource Types`` and ``Template " "Versions``." msgstr "" "Добавлена панель ``Генератор шаблонов`` (``Template Generator``). Какого то " "специального процесса её установки нет, и она будет доступна после установки " "панели управления Heat рядом с панелями ``Управление стеками``, ``Типы " "ресурсов`` и ``Версии шаблонов`` (``Stacks``, ``Resource Types`` и " "``Template Versions``)." msgid "Current Series Release Notes" msgstr "Примечания к текущему релизу" msgid "Heat Dashboard Release Notes" msgstr "Примечания о панели Heat" msgid "" "Heat support in OpenStack Dashboard is now split into a separated python " "package." msgstr "" "Поддержка Heat в панели управления OpenStack теперь выделена в отдельный " "Python модуль." msgid "" "Heat support in OpenStack Dashboard is now split into a separete package " "``heat-dashboard``. You need to install ``heat-dashboard`` after upgrading " "OpenStack Dashboard to Queens release and add ``enabled`` file for Heat " "Dashboard. For detail information, see https://docs.openstack.org/heat-" "dashboard/latest/." msgstr "" "Поддержка Heat в панели управления OpenStack теперь выделена в отдельный " "Python модуль ``heat-dashboard``. При обновлении панели управления OpenStack " "до релиза Queens, вам потребуется установить модуль ``heat-dashboard`` и " "добавить файл ``enabled`` для панели управления Heat. Дополнительную " "информацию можно получить на странице документации тут: https://docs." "openstack.org/heat-dashboard/latest/." msgid "" "If you need to update your queens heat-dashboard to rocky, you must copy the " "latest enabled file (_1650_project_template_generator_panel.py) to horizon/" "openstack_dashboard/enabled directory again. The enabled file for template " "generator has been changed a lot in rocky and it does not have compatibility " "with queens heat-dashboard. The background of this change is that SCSS and " "xstatic modules are started to be used rather than CSS and embedded " "JavaScript copies to follow Horizon’s contribution rule. [`bug 1753919 " "`__] [`bug 1755140 " "`__] [`bug 1755308 " "`__]" msgstr "" "Если вам нужно обновить панель управления Heat с релиза Queens до релиза " "Rocky, то вам понадобится снова скопировать ``enabled`` файл " "(_1650_project_template_generator_panel.py) в директорию horizon/" "openstack_dashboard/enabled. Этот ``enabled`` файл был достаточно сильно " "изменён в релизе Rocky и не совместим с панелью Heat релиза Queen. Это " "результат того, что мыстали использовать SCSS и xstatic модули вместо CSS и " "встроенного JavaScript, чтобы соответствовать правилам внесения изменений в " "панель управления Horizon. [`bug 1753919 `__] [`bug 1755140 `__] [`bug 1755308 `__]" msgid "New Features" msgstr "Новые особенности" msgid "Prelude" msgstr "Введение" msgid "Queens Series Release Notes" msgstr "Примечания к релизу Queens" msgid "Rocky Series Release Notes" msgstr "Примечания к релизу Rocky" msgid "Stein Series Release Notes" msgstr "Примечания к релизу Stein" msgid "Upgrade Notes" msgstr "Информация об обновлении" heat-dashboard-3.0.1/releasenotes/source/conf.py0000664000175000017500000002141513737340772021713 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Heat Dashboard Release Notes' copyright = u'2017, OpenStack Foundation' # openstackdocstheme options repository_name = 'openstack/heat-dashboard' bug_project = 'heat-dashboard' bug_tag = 'doc' html_last_updated_fmt = '%Y-%m-%d %H:%M' # Release notes are version independent, no need to set version and release release = '' version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'HeatDashboardReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'HeatDashboardReleaseNotes.tex', u'Heat Dashboard Release Notes Documentation', u'OpenStack Foundation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'HeatDashboardReleaseNotes', u'Heat Dashboard Release Notes Documentation', [u'OpenStack Foundation'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'HeatDashboardReleaseNotes', u'Heat Dashboard Release Notes Documentation', u'OpenStack Foundation', 'HeatDashboardReleaseNotes', 'Dashboard for Heat', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] heat-dashboard-3.0.1/releasenotes/source/stein.rst0000664000175000017500000000022113737340771022257 0ustar zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein heat-dashboard-3.0.1/tox.ini0000664000175000017500000000615313737340772015740 0ustar zuulzuul00000000000000[tox] envlist = py36,py37,pep8,releasenotes minversion = 3.1.1 skipsdist = True ignore_basepython_conflict = True [testenv] usedevelop = True basepython = python3 setenv = VIRTUAL_ENV={envdir} INTEGRATION_TESTS=0 NOSE_WITH_OPENSTACK=1 NOSE_OPENSTACK_COLOR=1 NOSE_OPENSTACK_RED=0.05 NOSE_OPENSTACK_YELLOW=0.025 NOSE_OPENSTACK_SHOW_ELAPSED=1 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python manage.py test heat_dashboard.test --settings=heat_dashboard.test.settings --exclude-tag integration [testenv:integration] # Run integration tests only passenv = AVCONV_INSTALLED setenv = PYTHONHASHSEED=0 INTEGRATION_TESTS=1 SELENIUM_HEADLESS=1 HORIZON_INTEGRATION_TESTS_CONFIG_FILE=heat_dashboard/test/integration/horizon.conf commands = python manage.py test heat_dashboard.test.integration --settings=heat_dashboard.test.settings --tag integration [testenv:pep8] commands = flake8 {posargs} [testenv:venv] commands = {posargs} [testenv:cover] commands = coverage erase coverage run --source=heat_dashboard {toxinidir}/manage.py test heat_dashboard.test.tests --settings=heat_dashboard.test.settings {posargs} coverage xml coverage html [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} -r{toxinidir}/doc/requirements.txt commands = python setup.py build_sphinx [testenv:pdf-docs] basepython = python3 envdir = {toxworkdir}/docs deps = {[testenv:docs]deps} whitelist_externals = make commands = sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:releasenotes] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,panel_template,dash_template,local_settings.py,*/local/*,*/test/test_plugins/*,.ropeproject,node_modules,.tmp # F405 TEMPLATES may be undefined, or defined from star imports # (because it is not easy to avoid this in openstack_dashboard.test.settings) # W504 line break after binary operator # (W503 and W504 are incompatible and we need to choose one of them. # Existing codes follows W503, so we disable W504.) ignore = F405,W504 max-complexity = 20 import-order-style = pep8 [flake8:local-plugins] extension = M322 = horizon.hacking.checks:no_mutable_default_args [testenv:npm] passenv = HOME DISPLAY CHROME_BIN commands = nodeenv -p npm install npm run {posargs:test} [doc8] # File extensions to check extensions = .rst, .yaml # Maximal line length should be 80 but we have some overlong lines. # Let's not get far more in. max-line-length = 80 # Disable some doc8 checks: # D000: Check RST validity # - cannot handle "none" for code-block directive ignore = D000 [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt heat-dashboard-3.0.1/HACKING.rst0000664000175000017500000000023713737340771016217 0ustar zuulzuul00000000000000openstack Style Commandments =============================================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ heat-dashboard-3.0.1/babel-django.cfg0000664000175000017500000000011513737340771017402 0ustar zuulzuul00000000000000[python: **.py] [django: **/templates/**.html] [django: **/templates/**.csv] heat-dashboard-3.0.1/lower-constraints.txt0000664000175000017500000000570213737340772020662 0ustar zuulzuul00000000000000amqp==2.1.1 appdirs==1.3.0 asn1crypto==0.23.0 Babel==2.3.4 cachetools==2.0.0 cffi==1.7.0 chardet==3.0.4 cliff==2.8.0 cmd2==0.8.0 colorama==0.3.9 contextlib2==0.4.0 coverage==4.0 cryptography==2.1 debtcollector==1.2.0 decorator==3.4.0 deprecation==1.0 Django==2.2 django-appconf==1.0.2 django-babel==0.6.2 django-compressor==2.0 django-pyscss==2.0.2 doc8==0.6.0 docutils==0.11 dogpile.cache==0.6.2 eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 futurist==1.2.0 greenlet==0.4.10 horizon==18.3.1 idna==2.6 iso8601==0.1.11 Jinja2==2.10 jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 keystoneauth1==3.4.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 mccabe==0.2.1 monotonic==0.6 msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 nodeenv==0.9.4 openstacksdk==0.11.2 os-client-config==1.28.0 os-service-types==1.2.0 osc-lib==1.8.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.19.2 oslo.i18n==3.15.3 oslo.log==3.36.0 oslo.messaging==5.29.0 oslo.middleware==3.31.0 oslo.policy==1.30.0 oslo.serialization==2.18.0 oslo.service==1.24.0 oslo.utils==3.33.0 osprofiler==2.3.0 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 pika==0.10.0 pika-pool==0.1.3 Pint==0.5 positional==1.2.1 prettytable==0.7.2 pycodestyle==2.3.1 pycparser==2.18 pyinotify==0.9.6 pymongo==3.0.2 pyOpenSSL==17.1.0 pyparsing==2.1.0 pyperclip==1.5.27 pyScss==1.3.7 python-cinderclient==5.0.0 python-dateutil==2.5.3 python-glanceclient==2.8.0 python-heatclient==1.10.0 python-keystoneclient==3.22.0 python-mimeparse==1.6.0 python-neutronclient==6.7.0 python-novaclient==9.1.0 python-swiftclient==3.2.0 pytz==2013.6 PyYAML==3.12 rcssmin==1.0.6 repoze.lru==0.7 requests==2.14.2 requestsexceptions==1.2.0 restructuredtext-lint==1.1.1 rfc3986==0.3.1 rjsmin==1.0.12 Routes==2.3.1 selenium==2.50.1 semantic-version==2.3.1 simplejson==3.5.1 six==1.10.0 statsd==3.2.1 stevedore==1.20.0 tenacity==3.2.1 termcolor==1.1.0 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 vine==1.1.4 warlock==1.2.0 WebOb==1.7.1 wrapt==1.7.0 XStatic==1.0.0 XStatic-Angular==1.5.8.0 XStatic-Angular-Bootstrap==2.2.0.0 XStatic-Angular-FileUpload==12.0.4.0 XStatic-Angular-Gettext==2.3.8.0 XStatic-Angular-lrdragndrop==1.0.2.2 XStatic-Angular-Schema-Form==0.8.13.0 XStatic-Bootstrap-Datepicker==1.3.1.0 XStatic-Bootstrap-SCSS==3.3.7.1 XStatic-bootswatch==3.3.7.0 XStatic-D3==3.5.17.0 XStatic-Font-Awesome==4.7.0.0 XStatic-Hogan==2.0.0.2 XStatic-Jasmine==2.4.1.1 XStatic-jQuery==1.8.2.1 XStatic-JQuery-Migrate==1.2.1.1 XStatic-jquery-ui==1.10.4.1 XStatic-JQuery.quicksearch==2.0.3.1 XStatic-JQuery.TableSorter==2.14.5.1 XStatic-JSEncrypt==2.3.1.1 XStatic-mdi==1.6.50.2 XStatic-objectpath==1.2.1.0 XStatic-Rickshaw==1.5.0.0 XStatic-roboto-fontface==0.5.0.0 XStatic-smart-table==1.4.13.2 XStatic-Spin==1.2.5.2 XStatic-term.js==0.0.7.0 XStatic-tv4==1.2.7.0 xstatic-angular-uuid===0.0.4.0 xstatic-angular-vis===4.16.0.0 xstatic-filesaver===1.3.2.0 xstatic-js-yaml===3.8.1.0 xstatic-json2yaml===0.1.1.0 xvfbwrapper==0.1.3 heat-dashboard-3.0.1/.eslintrc.js0000664000175000017500000002036213737340771016661 0ustar zuulzuul00000000000000module.exports = { "env": { "browser": true, "jasmine": true, }, "plugins": ["jasmine"], "globals": { "angular": true, "module": true, "inject": true, "saveAs": true, "json2yaml": true }, "extends": ["eslint:recommended", "plugin:jasmine/recommended"], "rules": { "accessor-pairs": "error", "array-bracket-newline": "off", "array-bracket-spacing": "off", "array-callback-return": "error", "array-element-newline": "off", "arrow-body-style": "error", "arrow-parens": "error", "arrow-spacing": "error", "block-scoped-var": "off", "block-spacing": "off", "brace-style": "off", "callback-return": "off", "camelcase": "off", "capitalized-comments": "off", "class-methods-use-this": "error", "comma-dangle": "off", "comma-spacing": "off", "comma-style": "off", "complexity": "off", "computed-property-spacing": [ "error", "never" ], "consistent-return": "off", "consistent-this": "off", "curly": "off", "default-case": "off", "dot-location": [ "error", "property" ], "dot-notation": "off", "eol-last": "off", "eqeqeq": "off", "for-direction": "error", "func-call-spacing": "off", "func-name-matching": "off", "func-names": "off", "func-style": "off", "function-paren-newline": "off", "generator-star-spacing": "error", "getter-return": "error", "global-require": "error", "guard-for-in": "off", "handle-callback-err": "error", "id-blacklist": "error", "id-length": "off", "id-match": "error", "indent": "off", "indent-legacy": "off", "init-declarations": "off", "jasmine/prefer-toHaveBeenCalledWith": "off", "jsx-quotes": "error", "key-spacing": "off", "keyword-spacing": "off", "line-comment-position": "off", "linebreak-style": [ "error", "unix" ], "lines-around-comment": "off", "lines-around-directive": "off", "lines-between-class-members": "error", "max-depth": "off", "max-len": "off", "max-lines": "off", "max-nested-callbacks": "error", "max-params": "off", "max-statements": "off", "max-statements-per-line": "off", "multiline-comment-style": "off", "multiline-ternary": "off", "new-parens": "off", "newline-after-var": "off", "newline-before-return": "off", "newline-per-chained-call": "off", "no-alert": "off", "no-array-constructor": "error", "no-await-in-loop": "error", "no-bitwise": "off", "no-buffer-constructor": "error", "no-caller": "error", "no-catch-shadow": "off", "no-confusing-arrow": "error", "no-continue": "off", "no-div-regex": "error", "no-duplicate-imports": "error", "no-else-return": "off", "no-empty-function": "off", "no-eq-null": "off", "no-eval": "error", "no-extend-native": "error", "no-extra-bind": "off", "no-extra-label": "off", "no-extra-parens": "off", "no-floating-decimal": "off", "no-implicit-coercion": [ "error", { "boolean": false, "number": false, "string": false } ], "no-implicit-globals": "off", "no-implied-eval": "error", "no-inline-comments": "off", "no-inner-declarations": [ "error", "functions" ], "no-invalid-this": "off", "no-iterator": "error", "no-label-var": "error", "no-lone-blocks": "off", "no-lonely-if": "off", "no-loop-func": "off", "no-magic-numbers": "off", "no-mixed-operators": "off", "no-mixed-requires": "error", "no-multi-assign": "off", "no-multi-spaces": "off", "no-multi-str": "error", "no-multiple-empty-lines": "off", "no-native-reassign": "error", "no-negated-condition": "off", "no-negated-in-lhs": "error", "no-nested-ternary": "off", "no-new": "error", "no-new-func": "off", "no-new-object": "error", "no-new-require": "error", "no-new-wrappers": "error", "no-octal-escape": "error", "no-param-reassign": "off", "no-path-concat": "error", "no-plusplus": "off", "no-process-env": "error", "no-process-exit": "error", "no-proto": "off", "no-prototype-builtins": "off", "no-redeclare": "off", "no-restricted-globals": "error", "no-restricted-imports": "error", "no-restricted-modules": "error", "no-restricted-properties": "error", "no-restricted-syntax": "error", "no-return-assign": "off", "no-return-await": "error", "no-script-url": "error", "no-self-compare": "error", "no-sequences": "off", "no-shadow": "off", "no-shadow-restricted-names": "off", "no-spaced-func": "off", "no-sync": "error", "no-tabs": "off", "no-template-curly-in-string": "error", "no-ternary": "off", "no-throw-literal": "off", "no-trailing-spaces": [ "error", { "ignoreComments": true, "skipBlankLines": true } ], "no-undef-init": "off", "no-undefined": "off", "no-underscore-dangle": "off", "no-unmodified-loop-condition": "off", "no-unneeded-ternary": "off", "no-unused-expressions": "off", "no-unused-vars": "off", "no-use-before-define": "off", "no-useless-call": "off", "no-useless-computed-key": "error", "no-useless-concat": "off", "no-useless-constructor": "error", "no-useless-rename": "error", "no-useless-return": "off", "no-var": "off", "no-void": "off", "no-warning-comments": "off", "no-whitespace-before-property": "error", "no-with": "error", "nonblock-statement-body-position": [ "error", "any" ], "object-curly-newline": "off", "object-curly-spacing": "off", "object-property-newline": "off", "object-shorthand": "off", "one-var": "off", "one-var-declaration-per-line": "off", "operator-assignment": "off", "operator-linebreak": "off", "padded-blocks": "off", "padding-line-between-statements": "error", "prefer-arrow-callback": "off", "prefer-const": "error", "prefer-destructuring": "off", "prefer-numeric-literals": "error", "prefer-promise-reject-errors": "error", "prefer-reflect": "off", "prefer-rest-params": "off", "prefer-spread": "off", "prefer-template": "off", "quote-props": "off", "quotes": "off", "radix": "off", "require-await": "error", "require-jsdoc": "off", "rest-spread-spacing": "error", "semi": "off", "semi-spacing": "off", "semi-style": "off", "sort-imports": "error", "sort-keys": "off", "sort-vars": "off", "space-before-blocks": "off", "space-before-function-paren": "off", "space-in-parens": "off", "space-infix-ops": "off", "space-unary-ops": "off", "spaced-comment": "off", "strict": "off", "switch-colon-spacing": "off", "symbol-description": "error", "template-curly-spacing": "error", "template-tag-spacing": "error", "unicode-bom": [ "error", "never" ], "valid-jsdoc": "off", "valid-typeof": [ "error", { "requireStringLiterals": false } ], "vars-on-top": "off", "wrap-iife": "off", "wrap-regex": "off", "yield-star-spacing": "error", "yoda": "off" } }; heat-dashboard-3.0.1/setup.py0000664000175000017500000000200613737340772016130 0ustar zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) heat-dashboard-3.0.1/.zuul.yaml0000664000175000017500000000177613737340772016374 0ustar zuulzuul00000000000000- job: name: heat-dashboard-integration-tests parent: horizon-integration-tests required-projects: - name: openstack/horizon - name: openstack/heat - name: openstack/python-heatclient - name: openstack/heat-dashboard roles: - zuul: openstack-infra/devstack - zuul: openstack/horizon irrelevant-files: - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ vars: devstack_plugins: heat: https://opendev.org/openstack/heat heat-dashboard: https://opendev.org/openstack/heat-dashboard devstack_services: horizon: true tox_envlist: integration - project: templates: - check-requirements - horizon-non-primary-django-jobs - horizon-nodejs10-jobs - openstack-lower-constraints-jobs - openstack-python3-ussuri-jobs-horizon - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - heat-dashboard-integration-tests: voting: false heat-dashboard-3.0.1/doc/0000775000175000017500000000000013737341153015157 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/doc/source/0000775000175000017500000000000013737341153016457 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/doc/source/index.rst0000664000175000017500000000074013737340771020326 0ustar zuulzuul00000000000000.. openstack documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. the main title comes from README.rst .. include:: ../../README.rst Contents -------- .. toctree:: :maxdepth: 2 Installation Guide Configuration Guide User Documentation contributor/index heat-dashboard-3.0.1/doc/source/configuration/0000775000175000017500000000000013737341153021326 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/doc/source/configuration/index.rst0000664000175000017500000000023113737340771023170 0ustar zuulzuul00000000000000================================== Heat Dashboard configuration guide ================================== .. toctree:: :maxdepth: 1 configuration heat-dashboard-3.0.1/doc/source/configuration/configuration.rst0000664000175000017500000000147713737340771024745 0ustar zuulzuul00000000000000============= Configuration ============= Heat Dashboard has configuration option as below. For more configurations, see `Configuration Guide `_ in the Horizon documentation. OPENSTACK_HEAT_STACK ~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 9.0.0(Mitaka) Default: .. code-block:: python { 'enable_user_pass': True } A dictionary of settings to use with heat stacks. Currently, the only setting available is "enable_user_pass", which can be used to disable the password field while launching the stack. Currently HEAT API needs user password to perform all the heat operations because in HEAT API trusts is not enabled by default. So, this setting can be set as "False" in-case HEAT uses trusts by default otherwise it needs to be set as "True". heat-dashboard-3.0.1/doc/source/install/0000775000175000017500000000000013737341153020125 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/doc/source/install/index.rst0000664000175000017500000000516513737340771022002 0ustar zuulzuul00000000000000================================= Heat Dashboard installation guide ================================= This page describes the manual installation of heat-dashboard, while distribution packages may provide more automated process. .. note:: This page assumes horizon has been installed. Horizon setup is beyond the scope of this page. Install Heat Dashboard with all relevant packages to your Horizon environment. .. code-block:: console pip install heat-dashboard In most cases, heat-dashboard is installed into your python "site-packages" directory like ``/usr/local/lib/python2.7/site-packages``. We refer to the directory of heat-dashboard as ```` below and it would be ``/heat_dashboard`` if installed via pip. The path varies depending on Linux distribution you use. To enable heat-dashboard plugin, you need to put horizon plugin setup files into horizon "enabled" directory. The plugin setup files are found in ``/enabled``. .. code-block:: console $ cp /enabled/_[1-9]*.py \ /usr/share/openstack-dashboard/openstack_dashboard/local/enabled .. note:: The directory ``local/enabled`` may be different depending on your environment or distribution used. The path above is one used in Ubuntu horizon package. Configure the policy file for heat-dashboard in OpenStack Dashboard ``local_settings.py``. .. code-block:: python POLICY_FILES['orchestration'] = '/conf/heat_policy.json' .. note:: If your ``local_settings.py`` has no ``POLICY_FILES`` yet, you need to define the default ``POLICY_FILES`` in ``local_settings.py``. If you use the example ``local_settings.py`` file from horizon, what you need is to uncomment ``POLICY_FILES`` (which contains the default values). You can also add additional configurations to ``local_settings.py``. For more detail, see :doc:`/configuration/configuration`. You can also find an example file at ``/heat_dashboard/local_settings.d``. Compile the translation message catalogs of heat-dashboard. .. code-block:: console $ cd $ python ./manage.py compilemessages Run the Django update commands. Note that ``compress`` is required when you enable compression. .. code-block:: console $ cd $ DJANGO_SETTINGS_MODULE=openstack_dashboard.settings python manage.py collectstatic --noinput $ DJANGO_SETTINGS_MODULE=openstack_dashboard.settings python manage.py compress --force Finally, restart your web server. For example, in case of apache: .. code-block:: console $ sudo service apache2 restart heat-dashboard-3.0.1/doc/source/user/0000775000175000017500000000000013737341153017435 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/doc/source/user/index.rst0000664000175000017500000000025513737340771021305 0ustar zuulzuul00000000000000================================= Heat Dashboard User Documentation ================================= .. toctree:: :maxdepth: 2 stacks.rst template_generator.rst heat-dashboard-3.0.1/doc/source/user/template_generator.rst0000664000175000017500000000353313737340771024061 0ustar zuulzuul00000000000000====================================== Generate a Heat Orchestration Template ====================================== Heat Dashboard provides a user-friendly interface to generate Heat Orchestration templates in a Drag and Drop way. Generate a template ~~~~~~~~~~~~~~~~~~~ #. Log in to the dashboard. #. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and click :guilabel:`Template Generator` category. #. Wait until the page is completely loaded. It may take several seconds. #. Click the dropdown menu of Template Version, and choose an appropriate version. #. Drag icons of resource types at the top of the page to the central canvas. #. Click icons on the canvas to specify properties of resources. #. Click EDIT button at the top of the canvas, to enable manipulate mode. #. When in manipulate mode, click on CONNECT button to add an edge between icons. #. Click edges to show details of connections. #. Click the Generate Template button at the top-right of the page and generated template will be shown in a text box. You can also add modification to the template here. #. Click CREATE STACK to jump to continue to :guilabel:`Launch Stack`. #. Click DOWNLOAD STACK to download the generated template. #. You can also click the Manage Drafts button at the top-right of the page, to temporarily save the editing canvas or to load a saved one. Currently Supported resource types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 types of resources are supported in the first release of Heat Dashboard. #. OS::Cinder::Volume #. OS::Cinder::VolumeAttachment #. OS::Heat::ResourceGroup #. OS::Neutron::FloatingIP #. OS::Neutron::FloatingIPAssociation #. OS::Neutron::Net #. OS::Neutron::Port #. OS::Neutron::Router #. OS::Neutron::RouterInterface #. OS::Neutron::SecurityGroup #. OS::Neutron::Subnet #. OS::Nova::KeyPair #. OS::Nova::Server heat-dashboard-3.0.1/doc/source/user/stacks.rst0000664000175000017500000001616013737340771021470 0ustar zuulzuul00000000000000======================== Launch and manage stacks ======================== OpenStack Orchestration is a service that you can use to orchestrate multiple composite cloud applications. This service supports the use of both the Amazon Web Services (AWS) CloudFormation template format through both a Query API that is compatible with CloudFormation and the native OpenStack Heat Orchestration Template (HOT) format through a REST API. These flexible template languages enable application developers to describe and automate the deployment of infrastructure, services, and applications. The templates enable creation of most OpenStack resource types, such as instances, floating IP addresses, volumes, security groups, and users. Once created, the resources are referred to as stacks. The template languages are described in the `Template Guide `_. Launch a stack ~~~~~~~~~~~~~~ #. Log in to the dashboard. #. Select the appropriate project from the drop down menu at the top left. #. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and click :guilabel:`Stacks` category. #. Click :guilabel:`Launch Stack`. #. In the :guilabel:`Select Template` dialog box, specify the following values: +---------------------------------------+-------------------------------+ | :guilabel:`Template Source` | Choose the source of the | | | template from the list. | +---------------------------------------+-------------------------------+ | :guilabel:`Template URL/File/Data` | Depending on the source that | | | you select, enter the URL, | | | browse to the file location, | | | or directly include the | | | template. | +---------------------------------------+-------------------------------+ | :guilabel:`Environment Source` | Choose the source of the | | | environment from the list. | | | The environment files contain | | | additional settings for the | | | stack. | +---------------------------------------+-------------------------------+ | :guilabel:`Environment File/Data` | Depending on the source that | | | you select, browse to the | | | file location, directly | | | include the environment | +---------------------------------------+-------------------------------+ #. Click :guilabel:`Next`. #. In the :guilabel:`Launch Stack` dialog box, specify the following values: +---------------------------------+---------------------------------+ | :guilabel:`Stack Name` | Enter a name to identify | | | the stack. | +---------------------------------+---------------------------------+ | :guilabel:`Creation Timeout` | Specify the number of minutes | | :guilabel:`(minutes)` | that can elapse before the | | | launch of the stack times out. | +---------------------------------+---------------------------------+ | :guilabel:`Rollback On Failure` | Select this check box if you | | | want the service to roll back | | | changes if the stack fails to | | | launch. | +---------------------------------+---------------------------------+ | :guilabel:`Password for user` | Specify the password that | | :guilabel:`"demo"` | the default user uses when the | | | stack is created. | +---------------------------------+---------------------------------+ | :guilabel:`DBUsername` | Specify the name of the | | | database user. | +---------------------------------+---------------------------------+ | :guilabel:`LinuxDistribution` | Specify the Linux distribution | | | that is used in the stack. | +---------------------------------+---------------------------------+ | :guilabel:`DBRootPassword` | Specify the root password for | | | the database. | +---------------------------------+---------------------------------+ | :guilabel:`KeyName` | Specify the name of the key pair| | | to use to log in to the stack. | +---------------------------------+---------------------------------+ | :guilabel:`DBName` | Specify the name of the | | | database. | +---------------------------------+---------------------------------+ | :guilabel:`DBPassword` | Specify the password of the | | | database. | +---------------------------------+---------------------------------+ | :guilabel:`InstanceType` | Specify the flavor for the | | | instance. | +---------------------------------+---------------------------------+ #. Click :guilabel:`Launch` to create a stack. The :guilabel:`Stacks` tab shows the stack. After the stack is created, click on the stack name to see the following details: Topology The topology of the stack. Overview The parameters and details of the stack. Resources The resources used by the stack. Events The events related to the stack. Template The template for the stack. Manage a stack ~~~~~~~~~~~~~~ #. Log in to the dashboard. #. Select the appropriate project from the drop down menu at the top left. #. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and click :guilabel:`Stacks` category. #. Select the stack that you want to update. #. Click :guilabel:`Change Stack Template`. #. In the :guilabel:`Select Template` dialog box, select the new template source or environment source. #. Click :guilabel:`Next`. The :guilabel:`Update Stack Parameters` window appears. #. Enter new values for any parameters that you want to update. #. Click :guilabel:`Update`. Delete a stack ~~~~~~~~~~~~~~ When you delete a stack, you cannot undo this action. #. Log in to the dashboard. #. Select the appropriate project from the drop down menu at the top left. #. On the :guilabel:`Project` tab, open the :guilabel:`Orchestration` tab and click :guilabel:`Stacks` category. #. Select the stack that you want to delete. #. Click :guilabel:`Delete Stack`. #. In the confirmation dialog box, click :guilabel:`Delete Stack` to confirm the deletion. heat-dashboard-3.0.1/doc/source/conf.py0000775000175000017500000000613013737340772017767 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'openstackdocstheme', # 'sphinx.ext.intersphinx', 'sphinxcontrib.rsvgconverter', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Heat Dashboard' copyright = u'2017, OpenStack Developers' # openstackdocstheme options repository_name = 'openstack/heat-dashboard' bug_project = '992' bug_tag = 'doc' html_last_updated_fmt = '%Y-%m-%d %H:%M' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] html_theme = 'openstackdocs' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'doc-heat-dashboard.tex', u'Heat Dashboard Documentation', u'OpenStack Developers', 'manual', True), ] latex_domain_indices = False latex_elements = { # Additional stuff for the LaTeX preamble. # openany: Skip blank pages in generated PDFs 'extraclassoptions': 'openany,oneside', 'makeindex': '', 'printindex': '', 'preamble': r'\setcounter{tocdepth}{3}', } man_pages = [ ('index', u'Heat Dashboard Documentation', 'Documentation for Heat Dashboard plugin to Openstack\ Dashboard (Horizon)', [u'OpenStack'], 1) ] # Example configuration for intersphinx: refer to the Python standard library. # intersphinx_mapping = {'http://docs.python.org/': None} heat-dashboard-3.0.1/doc/source/contributor/0000775000175000017500000000000013737341153021031 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/doc/source/contributor/index.rst0000664000175000017500000000021613737340771022676 0ustar zuulzuul00000000000000=========================== Contributor Documentation =========================== .. toctree:: :maxdepth: 2 contributing devstack heat-dashboard-3.0.1/doc/source/contributor/contributing.rst0000664000175000017500000000122713737340771024301 0ustar zuulzuul00000000000000================= How to Contribute ================= Contributor License Agreement ----------------------------- .. index:: single: license; agreement In order to contribute to the Heat Dashboard project, you need to have signed OpenStack's contributor's agreement. .. seealso:: * https://docs.openstack.org/infra/manual/developers.html * https://wiki.openstack.org/CLA Project Hosting Details ------------------------- Bug tracker https://storyboard.openstack.org/#!/project/992 Code Hosting https://opendev.org/openstack/heat-dashboard/ Code Review https://review.opendev.org/#/q/status:open+project:openstack/heat-dashboard,n,z heat-dashboard-3.0.1/doc/source/contributor/devstack.rst0000664000175000017500000000052413737340771023375 0ustar zuulzuul00000000000000============================== Use Heat Dashboard in DevStack ============================== Set up your ``local.conf`` to enable heat-dashboard:: [[local|localrc]] enable_plugin heat-dashboard https://opendev.org/openstack/heat-dashboard .. note:: You also need to install Heat itself into DevStack to use Heat Dashboard. heat-dashboard-3.0.1/doc/requirements.txt0000664000175000017500000000064113737340772020452 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. openstackdocstheme>=1.18.1 # Apache-2.0 sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD reno>=2.5.0 # Apache-2.0 sphinxcontrib-httpdomain>=1.3.0 # BSD sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD heat-dashboard-3.0.1/CONTRIBUTING.rst0000664000175000017500000000125113737340771017057 0ustar zuulzuul00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps in this page: https://docs.openstack.org/infra/manual/developers.html If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on StoryBoard, not GitHub and Launchpad: https://storyboard.openstack.org/#!/project/992 heat-dashboard-3.0.1/package.json0000664000175000017500000000260513737340771016710 0ustar zuulzuul00000000000000{ "name": "heat-dashboard", "version": "0.0.0", "description": "OpenStack Heat Dashboard - Angular", "main": "karma.conf.js", "directories": { "doc": "doc" }, "devDependencies": { "@appscode/json2yaml": "0.1.0", "@cgross/angular-notify": "2.5.1", "angular": "1.5.8", "angular-animate": "1.5.8", "angular-aria": "1.5.8", "angular-material": "1.1.5", "angular-messages": "1.5.8", "angular-mocks": "1.5.8", "angular-notify": "1.0.0", "angular-sanitize": "1.5.8", "angular-ui-bootstrap": "2.5.0", "angular-ui-router": "0.4.3", "angular-uuid": "0.0.4", "angular-visjs": "4.16.0", "eslint": "4.9.0", "eslint-plugin-angular": "3.1.1", "eslint-plugin-jasmine": "2.9.1", "file-saver": "1.3.2", "jasmine": "2.8.0", "jasmine-core": "2.8.0", "js-yaml": "3.8.1", "json2yaml": "1.1.0", "karma": "1.7.1", "karma-chrome-launcher": "2.2.0", "karma-coverage": "1.1.1", "karma-firefox-launcher": "1.0.1", "karma-jasmine": "1.1.0", "karma-ng-html2js-preprocessor": "1.0.0", "vis": "4.3.0" }, "scripts": { "test": "if [ -z $CHROME_BIN ];then export CHROME_BIN=/usr/bin/chromium-browser;fi && karma start karma.conf.js --single-run", "lint": "eslint --no-color heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/" }, "author": "", "license": "Apache-2.0" } heat-dashboard-3.0.1/.eslintignore0000664000175000017500000000012413737340771017117 0ustar zuulzuul00000000000000heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/libs/* heat-dashboard-3.0.1/PKG-INFO0000664000175000017500000000275313737341153015516 0ustar zuulzuul00000000000000Metadata-Version: 1.2 Name: heat-dashboard Version: 3.0.1 Summary: Heat Management Dashboard Home-page: https://docs.openstack.org/heat-dashboard/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ========================== Welcome to Heat Dashboard! ========================== Heat dashboard is a horizon plugin for Heat. * License: Apache license * Documentation: https://docs.openstack.org/heat-dashboard/latest/ * Source: https://opendev.org/openstack/heat-dashboard * Bugs: https://storyboard.openstack.org/#!/project/openstack/heat-dashboard * Release Notes: https://docs.openstack.org/releasenotes/heat-dashboard Team and repository tags ------------------------ .. image:: https://governance.openstack.org/tc/badges/heat-dashboard.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Python: >=3.6 heat-dashboard-3.0.1/AUTHORS0000664000175000017500000001555013737341152015467 0ustar zuulzuul00000000000000Aaron Sahlin Abishek Subramanian Akihiro MOTOKI Akihiro Motoki Akihiro Motoki Alex Gaynor Ana Krivokapic Ana Malagon Andreas Jaeger Andrew Lazarev Antoine Abélard Ashish Chandra AvnishPal Beth Elwell Bo Wang Brianna Poulos Béla Vancsics Cady_Chen Chad Roberts Charles Short ChenZheng Christian Berendt Chuck Short Chuck Short Cindy Lu Cody A.W. Somerville CristianFiorentino David Lyle David Lyle David Lyle Diana Clarke Diana Whitten Dirk Mueller Doug Fish Doug Hellmann Eddie Ramirez Eyal Felipe Reyes Fengqian Gao Gabriel Hurley Gary W. Smith George Peristerakis Gloria Gu Gábor Antal He Yongli Itxaka Ivan Kolodyazhny Jack Choy James E. Blair James E. Blair Janet Yu Jason E. Rist Jasper Capel Jean-Philippe Evrard Jeremy Stanley JiaHao Li Jianing YANG Jiri Tomasek JiyeYu Joe Gordon Jordan OMara Julie Pichon Julien Danjou Justin Pomeroy KC Wang KC Wang Kahou Lei Kanagaraj Manickam Kazunori Shinohara Keiichi Hikita Kenji Ishii Kevin Fox Kieran Spear Kun Huang Kun Huang LIU Yulong LIU-Yulong Ladislav Smola Lajos Katona Laura Frank Leandro I. Costantino Lin Hua Cheng LiuNanke Lucas Palm Mark McClain Masco Kaliyamoorthy Matt Borland Matt Borland Matt Riedemann Matthias Runge Maxim Nestratov Maxime Vidori Michael Dovgal Michael Krotscheck Michal Dulko Miguel Grinberg Mike Hagedorn Monty Taylor Nathan Reller Omri Gazitt Ondřej Nový OpenStack Release Bot Paul Belanger Paul Karikh Pavlo Shchelokovskyy Peter Belanyi Radomir Dopieralski Rajat Vig Ramaraja Revon Mathews Richard Jones Rob Cresswell Rob Raymond Robert Myers Roman Rader Russell Sim Sam Betts Samantha Blanco Saro Chandra Bhooshan Sascha Peilicke Sascha Peilicke Sean Dague Sergei Chipiga Sergey Lukjanov Shane Wang Steve Baker Steve Leon Steve McLellan Sushil Kumar Tatiana Mazur Tatiana Ovchinnikova Thai Tran Thomas Bechtold Thomas Goirand Tim Schnell Timur Sufiev Travis Tripp Trinh Nguyen Tyr Johanson Vic Howard Victor Stinner Vlad Okhrimenko Vu Cong Tuan Wu Wenxiang Xinni Ge Ying Zuo Yves-Gwenael Bourhis Zhenguo Niu ZhijunWei absubram chen.qiaomin@99cloud.net chenxing dixiaoli eric huang.zhiping irisayame jacky06 jing.liuqing jlopezgu jolie kaz_shinohara kenji-ishii lawrancejing lin-hua-cheng lin-hua-cheng lingyongxu liyingjun lvxianguo manchandavishal mareklycka nikunj2512 pengyuesheng ricolin ricolin root shizhihui simon tianliang wangbo wangqi wei.ying wu.chunyang xurong00037997 yaraat zhang.lei zhangboye zhaozhilong zhu.rong zhurong heat-dashboard-3.0.1/heat_dashboard/0000775000175000017500000000000013737341153017342 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/0000775000175000017500000000000013737341153020321 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/0000775000175000017500000000000013737341153022644 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/horizon.conf0000664000175000017500000000032713737340771025212 0ustar zuulzuul00000000000000[plugin] is_plugin=True plugin_page_path=heat_dashboard.test.integration.pages plugin_page_structure='{"Project": {"Orchestration": {"_": ["Stacks", "Resource Types", "Template Versions", "Template Generator"]}}}' heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/0000775000175000017500000000000013737341153023743 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/__init__.py0000664000175000017500000000000013737340771026047 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/0000775000175000017500000000000013737341153025411 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/0000775000175000017500000000000013737341153030275 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/stackspage.py0000664000175000017500000000147013737340771033003 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test.integration_tests.pages import basepage class StacksPage(basepage.BaseNavigationPage): def __init__(self, driver, conf): super(StacksPage, self).__init__(driver, conf) self._page_title = "Stacks" ././@LongLink0000000000000000000000000000015100000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/templateversionspage.pyheat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/templateversionspag0000664000175000017500000000152713737340771034326 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test.integration_tests.pages import basepage class TemplateversionsPage(basepage.BaseNavigationPage): def __init__(self, driver, conf): super(TemplateversionsPage, self).__init__(driver, conf) self._page_title = "Template Versions" heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/__init__.py0000664000175000017500000000000013737340771032401 0ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000015200000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/templategeneratorpage.pyheat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/templategeneratorpa0000664000175000017500000000153213737340771034271 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test.integration_tests.pages import basepage class TemplategeneratorPage(basepage.BaseNavigationPage): def __init__(self, driver, conf): super(TemplategeneratorPage, self).__init__(driver, conf) self._page_title = "Template Generator" ././@LongLink0000000000000000000000000000014600000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/resourcetypespage.pyheat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/orchestration/resourcetypespage.p0000664000175000017500000000151613737340771034237 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test.integration_tests.pages import basepage class ResourcetypesPage(basepage.BaseNavigationPage): def __init__(self, driver, conf): super(ResourcetypesPage, self).__init__(driver, conf) self._page_title = "Resource Types" heat-dashboard-3.0.1/heat_dashboard/test/integration/pages/project/__init__.py0000664000175000017500000000000013737340771027515 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/__init__.py0000664000175000017500000000000013737340771024750 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/integration/test_basic.py0000664000175000017500000000346013737340771025346 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test.integration_tests import helpers class TestHeatDashboardInstalled(helpers.TestCase): def test_alarms_page_opened(self): stacks_page = self.home_pg.go_to_project_orchestration_stackspage() self.assertEqual(stacks_page.page_title, 'Stacks - OpenStack Dashboard') def test_resource_types_page_opened(self): resource_types_page = ( self.home_pg.go_to_project_orchestration_resourcetypespage()) self.assertEqual(resource_types_page.page_title, 'Resource Types - OpenStack Dashboard') def test_template_versions_page_opened(self): template_versions_page = ( self.home_pg.go_to_project_orchestration_templateversionspage()) self.assertEqual(template_versions_page.page_title, 'Template Versions - OpenStack Dashboard') def test_template_generator_page_opened(self): template_generator_page = ( self.home_pg.go_to_project_orchestration_templategeneratorpage()) # TODO(e0ne): fix page title once Heat dashaboard will be updated self.assertEqual(template_generator_page.page_title, 'Horizon - OpenStack Dashboard') heat-dashboard-3.0.1/heat_dashboard/test/helpers.py0000664000175000017500000003626013737340771022351 0ustar zuulzuul00000000000000# Copyright 2012 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # # Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from importlib import import_module import os import traceback import unittest from unittest import mock import django from django.conf import settings from django.contrib.messages.storage import default_storage from django.core.handlers import wsgi from django.test.client import RequestFactory from django.utils import http from heatclient import client as heat_client from keystoneclient.v2_0 import client as keystone_client from neutronclient.v2_0 import client as neutron_client from openstack_auth import user from openstack_auth import utils from requests.packages.urllib3.connection import HTTPConnection from horizon.test import helpers as horizon_helpers from openstack_dashboard import api as project_api from openstack_dashboard import context_processors from openstack_dashboard.test import helpers from heat_dashboard import api from heat_dashboard.test.test_data import utils as test_utils # Makes output of failing mox tests much easier to read. wsgi.WSGIRequest.__repr__ = lambda self: "" # Shortcuts to avoid importing openstack_dashboard.test.helper and # for backwards compatibility. create_mocks = helpers.create_mocks IsA = helpers.IsA IsHttpRequest = helpers.IsHttpRequest def _apply_panel_mocks(patchers=None): """Global mocks on panels that get called on all views.""" if patchers is None: patchers = {} mocked_methods = getattr(settings, 'TEST_GLOBAL_MOCKS_ON_PANELS', {}) for name, mock_config in mocked_methods.items(): method = mock_config['method'] mock_params = {} for param in ['return_value', 'side_effect']: if param in mock_config: mock_params[param] = mock_config[param] patcher = mock.patch(method, **mock_params) patcher.start() patchers[name] = patcher return patchers class RequestFactoryWithMessages(RequestFactory): def get(self, *args, **kwargs): req = super(RequestFactoryWithMessages, self).get(*args, **kwargs) req.user = utils.get_user(req) req.session = [] req._messages = default_storage(req) return req def post(self, *args, **kwargs): req = super(RequestFactoryWithMessages, self).post(*args, **kwargs) req.user = utils.get_user(req) req.session = [] req._messages = default_storage(req) return req @unittest.skipIf(os.environ.get('SKIP_UNITTESTS', False), "The SKIP_UNITTESTS env variable is set.") class TestCase(horizon_helpers.TestCase): """Specialized base test case class for Horizon. It gives access to numerous additional features: * A full suite of test data through various attached objects and managers (e.g. ``self.servers``, ``self.user``, etc.). See the docs for :class:`~openstack_dashboard.test.test_data.utils.TestData` for more information. * The ``mox`` mocking framework via ``self.mox``. * A set of request context data via ``self.context``. * A ``RequestFactory`` class which supports Django's ``contrib.messages`` framework via ``self.factory``. * A ready-to-go request object via ``self.request``. * The ability to override specific time data controls for easier testing. * Several handy additional assertion methods. """ # To force test failures when unmocked API calls are attempted, provide # boolean variable to store failures missing_mocks = False def fake_conn_request(self): # print a stacktrace to illustrate where the unmocked API call # is being made from traceback.print_stack() # forcing a test failure for missing mock self.missing_mocks = True def setUp(self): self._real_conn_request = HTTPConnection.connect HTTPConnection.connect = self.fake_conn_request self._real_context_processor = context_processors.openstack context_processors.openstack = lambda request: self.context self.patchers = _apply_panel_mocks() super(TestCase, self).setUp() def _setup_test_data(self): super(TestCase, self)._setup_test_data() test_utils.load_test_data(self) self.context = { 'authorized_tenants': self.tenants.list(), 'JS_CATALOG': context_processors.get_js_catalog(settings), } def _setup_factory(self): # For some magical reason we need a copy of this here. self.factory = RequestFactoryWithMessages() def _setup_user(self, **kwargs): self._real_get_user = utils.get_user tenants = self.context['authorized_tenants'] base_kwargs = { 'id': self.user.id, 'token': self.token, 'username': self.user.name, 'domain_id': self.domain.id, 'user_domain_name': self.domain.name, 'tenant_id': self.tenant.id, 'service_catalog': self.service_catalog, 'authorized_tenants': tenants } base_kwargs.update(kwargs) self.setActiveUser(**base_kwargs) def _setup_request(self): super(TestCase, self)._setup_request() self.request.session['token'] = self.token.id def tearDown(self): HTTPConnection.connect = self._real_conn_request context_processors.openstack = self._real_context_processor utils.get_user = self._real_get_user mock.patch.stopall() super(TestCase, self).tearDown() # cause a test failure if an unmocked API call was attempted if self.missing_mocks: raise AssertionError("An unmocked API call was made.") def setActiveUser(self, id=None, token=None, username=None, tenant_id=None, service_catalog=None, tenant_name=None, roles=None, authorized_tenants=None, enabled=True, domain_id=None, user_domain_name=None): def get_user(request): return user.User(id=id, token=token, user=username, domain_id=domain_id, user_domain_name=user_domain_name, tenant_id=tenant_id, tenant_name=tenant_name, service_catalog=service_catalog, roles=roles, enabled=enabled, authorized_tenants=authorized_tenants, endpoint=settings.OPENSTACK_KEYSTONE_URL) utils.get_user = get_user def assertRedirectsNoFollow(self, response, expected_url): """Check for redirect. Asserts that the given response issued a 302 redirect without processing the view which is redirected to. """ if django.VERSION >= (1, 9): loc = str(response._headers.get('location', None)[1]) loc = http.urlunquote(loc) expected_url = http.urlunquote(expected_url) self.assertEqual(loc, expected_url) else: self.assertEqual(response._headers.get('location', None), ('Location', settings.TESTSERVER + expected_url)) self.assertEqual(response.status_code, 302) def assertNoFormErrors(self, response, context_name="form"): """Checks for no form errors. Asserts that the response either does not contain a form in its context, or that if it does, that form has no errors. """ context = getattr(response, "context", {}) if not context or context_name not in context: return True errors = response.context[context_name]._errors assert len(errors) == 0, \ "Unexpected errors were found on the form: %s" % errors def assertFormErrors(self, response, count=0, message=None, context_name="form"): """Check for form errors. Asserts that the response does contain a form in its context, and that form has errors, if count were given, it must match the exact numbers of errors """ context = getattr(response, "context", {}) assert (context and context_name in context), \ "The response did not contain a form." errors = response.context[context_name]._errors if count: assert len(errors) == count, \ "%d errors were found on the form, %d expected" % \ (len(errors), count) if message and message not in str(errors): self.fail("Expected message not found, instead found: %s" % ["%s: %s" % (key, [e for e in field_errors]) for (key, field_errors) in errors.items()]) else: assert len(errors) > 0, "No errors were found on the form" def assertStatusCode(self, response, expected_code): """Validates an expected status code. Matches camel case of other assert functions """ if response.status_code == expected_code: return self.fail('status code %r != %r: %s' % (response.status_code, expected_code, response.content)) def assertItemsCollectionEqual(self, response, items_list): self.assertEqual(response.json, {"items": items_list}) def getAndAssertTableRowAction(self, response, table_name, action_name, row_id): table = response.context[table_name + '_table'] rows = list(filter(lambda x: x.id == row_id, table.data)) self.assertEqual(1, len(rows), "Did not find a row matching id '%s'" % row_id) row_actions = table.get_row_actions(rows[0]) actions = list(filter(lambda x: x.name == action_name, row_actions)) msg_args = (action_name, table_name, row_id) self.assertGreater( len(actions), 0, "No action named '%s' found in '%s' table for id '%s'" % msg_args) self.assertEqual( 1, len(actions), "Multiple actions named '%s' found in '%s' table for id '%s'" % msg_args) return actions[0] def getAndAssertTableAction(self, response, table_name, action_name): table = response.context[table_name + '_table'] table_actions = table.get_table_actions() actions = list(filter(lambda x: x.name == action_name, table_actions)) msg_args = (action_name, table_name) self.assertGreater( len(actions), 0, "No action named '%s' found in '%s' table" % msg_args) self.assertEqual( 1, len(actions), "More than one action named '%s' found in '%s' table" % msg_args) return actions[0] @staticmethod def mock_rest_request(**args): mock_args = { 'user.is_authenticated.return_value': True, 'is_ajax.return_value': True, 'policy.check.return_value': True, 'body': '' } mock_args.update(args) return mock.Mock(**mock_args) class BaseAdminViewTests(TestCase): """Sets an active user with the "admin" role. For testing admin-only views and functionality. """ def setActiveUser(self, *args, **kwargs): if "roles" not in kwargs: kwargs['roles'] = [self.roles.admin._info] super(BaseAdminViewTests, self).setActiveUser(*args, **kwargs) def setSessionValues(self, **kwargs): settings.SESSION_ENGINE = 'django.contrib.sessions.backends.file' engine = import_module(settings.SESSION_ENGINE) store = engine.SessionStore() for key in kwargs: store[key] = kwargs[key] self.request.session[key] = kwargs[key] store.save() self.session = store self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key class APITestCase(TestCase): """Testing APIs. For use with tests which deal with the underlying clients rather than stubbing out the openstack_dashboard.api.* methods. """ def setUp(self): super(APITestCase, self).setUp() utils.patch_middleware_get_user() def fake_keystoneclient(request, admin=False): """Returns the stub keystoneclient. Only necessary because the function takes too many arguments to conveniently be a lambda. """ return self.stub_keystoneclient() # Store the original clients self._original_keystoneclient = project_api.keystone.keystoneclient self._original_heatclient = api.heat.heatclient # Replace the clients with our stubs. project_api.keystone.keystoneclient = fake_keystoneclient api.heat.heatclient = (lambda request, password=None: self.stub_heatclient()) def tearDown(self): super(APITestCase, self).tearDown() project_api.keystone.keystoneclient = self._original_keystoneclient api.heat.heatclient = self._original_heatclient def stub_keystoneclient(self): if not hasattr(self, "keystoneclient"): keystone_client.Client = mock.Mock() # NOTE(saschpe): Mock properties, MockObject.__init__ ignores them: keystone_client.Client.auth_token = 'foo' keystone_client.Client.service_catalog = None keystone_client.Client.tenant_id = '1' keystone_client.Client.tenant_name = 'tenant_1' keystone_client.Client.management_url = "" keystone_client.Client.__dir__ = lambda: [] self.keystoneclient = keystone_client.Client return self.keystoneclient def stub_neutronclient(self): if not hasattr(self, "neutronclient"): neutron_client.Client = mock.Mock() self.neutronclient = neutron_client.Client return self.neutronclient def stub_heatclient(self): if not hasattr(self, "heatclient"): heat_client.Client = mock.Mock() self.heatclient = heat_client.Client return self.heatclient # Need this to test both Glance API V1 and V2 versions class ResetImageAPIVersionMixin(object): def setUp(self): super(ResetImageAPIVersionMixin, self).setUp() project_api.glance.VERSIONS.clear_active_cache() def tearDown(self): project_api.glance.VERSIONS.clear_active_cache() super(ResetImageAPIVersionMixin, self).tearDown() heat-dashboard-3.0.1/heat_dashboard/test/tests/0000775000175000017500000000000013737341153021463 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/tests/__init__.py0000664000175000017500000000000013737340771023567 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/tests/api/0000775000175000017500000000000013737341153022234 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/tests/api/test_heat_rest.py0000664000175000017500000000510313737340771025627 0ustar zuulzuul00000000000000# (c) Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from unittest import mock from heat_dashboard.api.rest import heat from heat_dashboard.test import helpers as test from openstack_dashboard import api class ValidateRestTestCase(test.TestCase): @mock.patch.object(heat.api, 'heat') def test_validate_post(self, hc): body = '''{"template_url":"http://localhost/template.yaml"}''' request = self.mock_rest_request(body=body) hc.template_validate.return_value = ({'Description': 'foo'}) response = heat.Validate().post(request) self.assertStatusCode(response, 200) self.assertEqual(response.json, {"Description": "foo"}) kwargs = json.loads(body) hc.template_validate.assert_called_once_with(request, **kwargs) class HeatRestTestCase(test.TestCase): # # Services # @test.create_mocks({api.base: ('is_service_enabled',)}) @mock.patch.object(heat.api, 'heat') def test_services_get(self, hc): request = self.mock_rest_request(GET={}) self.mock_is_service_enabled.return_value = True hc.service_list.return_value = [ mock.Mock(**{'to_dict.return_value': {'id': '1'}}), mock.Mock(**{'to_dict.return_value': {'id': '2'}}) ] response = heat.Services().get(request) self.assertStatusCode(response, 200) self.assertEqual(response.content.decode('utf-8'), '{"items": [{"id": "1"}, {"id": "2"}]}') hc.service_list.assert_called_once_with(request) self.mock_is_service_enabled.assert_called_once_with( request, 'orchestration') @test.create_mocks({api.base: ('is_service_enabled',)}) def test_services_get_disabled(self): request = self.mock_rest_request(GET={}) self.mock_is_service_enabled.return_value = False response = heat.Services().get(request) self.assertStatusCode(response, 501) self.mock_is_service_enabled.assert_called_once_with( request, 'orchestration') heat-dashboard-3.0.1/heat_dashboard/test/tests/api/test_heat.py0000664000175000017500000003312013737340771024572 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import io from unittest import mock from django.conf import settings from django.test.utils import override_settings from heat_dashboard import api from heat_dashboard.test import helpers as test from horizon import exceptions class HeatApiTests(test.APITestCase): def test_stack_list(self): api_stacks = self.stacks.list() limit = getattr(settings, 'API_RESULT_LIMIT', 1000) heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.list.return_value = iter(api_stacks) stacks, has_more, has_prev = api.heat.stacks_list(self.request) heatclient.stacks.list.assert_called_once_with(limit=limit, sort_dir='desc', sort_key='created_at', ) self.assertCountEqual(stacks, api_stacks) self.assertFalse(has_more) self.assertFalse(has_prev) @override_settings(API_RESULT_PAGE_SIZE=2) def test_stack_list_sort_options(self): # Verify that sort_dir and sort_key work api_stacks = self.stacks.list() limit = getattr(settings, 'API_RESULT_LIMIT', 1000) sort_dir = 'asc' sort_key = 'size' heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.list.return_value = iter(api_stacks) stacks, has_more, has_prev = api.heat.stacks_list(self.request, sort_dir=sort_dir, sort_key=sort_key) self.assertCountEqual(stacks, api_stacks) self.assertFalse(has_more) self.assertFalse(has_prev) heatclient.stacks.list.assert_called_once_with( limit=limit, sort_dir=sort_dir, sort_key=sort_key,) @override_settings(API_RESULT_PAGE_SIZE=20) def test_stack_list_pagination_less_page_size(self): api_stacks = self.stacks.list() page_size = settings.API_RESULT_PAGE_SIZE sort_dir = 'desc' sort_key = 'created_at' heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.list.return_value = iter(api_stacks) stacks, has_more, has_prev = api.heat.stacks_list(self.request, sort_dir=sort_dir, sort_key=sort_key, paginate=True) expected_stacks = api_stacks[:page_size] self.assertCountEqual(stacks, expected_stacks) self.assertFalse(has_more) self.assertFalse(has_prev) heatclient.stacks.list.assert_called_once_with( limit=page_size + 1, sort_dir=sort_dir, sort_key=sort_key) @override_settings(API_RESULT_PAGE_SIZE=10) def test_stack_list_pagination_equal_page_size(self): api_stacks = self.stacks.list() page_size = settings.API_RESULT_PAGE_SIZE sort_dir = 'desc' sort_key = 'created_at' heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.list.return_value = iter(api_stacks) stacks, has_more, has_prev = api.heat.stacks_list(self.request, sort_dir=sort_dir, sort_key=sort_key, paginate=True) expected_stacks = api_stacks[:page_size] self.assertCountEqual(stacks, expected_stacks) self.assertFalse(has_more) self.assertFalse(has_prev) heatclient.stacks.list.assert_called_once_with( limit=page_size + 1, sort_dir=sort_dir, sort_key=sort_key,) @override_settings(API_RESULT_PAGE_SIZE=2) def test_stack_list_pagination_marker(self): page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 20) sort_dir = 'desc' sort_key = 'created_at' marker = 'nonsense' api_stacks = self.stacks.list() heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.list.return_value = iter(api_stacks[:page_size + 1]) stacks, has_more, has_prev = api.heat.stacks_list(self.request, marker=marker, paginate=True, sort_dir=sort_dir, sort_key=sort_key,) self.assertEqual(len(stacks), page_size) self.assertCountEqual(stacks, api_stacks[:page_size]) self.assertTrue(has_more) self.assertTrue(has_prev) heatclient.stacks.list.assert_called_once_with( limit=page_size + 1, marker=marker, sort_dir=sort_dir, sort_key=sort_key,) @override_settings(API_RESULT_PAGE_SIZE=2) def test_stack_list_pagination_marker_prev(self): page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 20) sort_dir = 'asc' sort_key = 'created_at' marker = 'nonsense' api_stacks = self.stacks.list() heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.list.return_value = iter(api_stacks[:page_size + 1]) stacks, has_more, has_prev = api.heat.stacks_list(self.request, marker=marker, paginate=True, sort_dir=sort_dir, sort_key=sort_key,) self.assertEqual(len(stacks), page_size) self.assertCountEqual(stacks, api_stacks[:page_size]) self.assertTrue(has_more) self.assertTrue(has_prev) heatclient.stacks.list.assert_called_once_with( limit=page_size + 1, marker=marker, sort_dir=sort_dir, sort_key=sort_key,) def test_template_get(self): api_stacks = self.stacks.list() stack_id = api_stacks[0].id mock_data_template = self.stack_templates.list()[0] heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.template.return_value = mock_data_template template = api.heat.template_get(self.request, stack_id) self.assertEqual(mock_data_template.data, template.data) heatclient.stacks.template.assert_called_once_with(stack_id) def test_stack_create(self): api_stacks = self.stacks.list() stack = api_stacks[0] heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() form_data = {'timeout_mins': 600} password = 'secret' heatclient.stacks.create.return_value = stack returned_stack = api.heat.stack_create(self.request, password, **form_data) from heatclient.v1 import stacks self.assertIsInstance(returned_stack, stacks.Stack) heatclient.stack.create_assert_called_once_with(**form_data) def test_stack_update(self): api_stacks = self.stacks.list() stack = api_stacks[0] stack_id = stack.id heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() form_data = {'timeout_mins': 600} password = 'secret' heatclient.stacks.update.return_value = stack returned_stack = api.heat.stack_update(self.request, stack_id, password, **form_data) from heatclient.v1 import stacks self.assertIsInstance(returned_stack, stacks.Stack) heatclient.stacks.update.assert_called_once_with( stack_id, **form_data) def test_snapshot_create(self): stack_id = self.stacks.first().id snapshot_create = self.stack_snapshot_create.list()[0] heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.snapshot.return_value = snapshot_create returned_snapshot_create_info = api.heat.snapshot_create(self.request, stack_id) self.assertEqual(returned_snapshot_create_info, snapshot_create) heatclient.stacks.snapshot.assert_called_once_with(stack_id) def test_snapshot_list(self): stack_id = self.stacks.first().id snapshot_list = self.stack_snapshot.list() heatclient = self.stub_heatclient() heatclient.stacks = mock.Mock() heatclient.stacks.snapshot_list.return_value = snapshot_list returned_snapshots = api.heat.snapshot_list(self.request, stack_id) self.assertCountEqual(returned_snapshots, snapshot_list) heatclient.stacks.snapshot_list.assert_called_once_with(stack_id) def test_get_template_files_with_template_data(self): tmpl = ''' # comment heat_template_version: 2013-05-23 resources: server1: type: OS::Nova::Server properties: flavor: m1.medium image: cirros ''' expected_files = {} files = api.heat.get_template_files(template_data=tmpl)[0] self.assertEqual(files, expected_files) @mock.patch('heatclient.common.utils.read_url_content') def test_get_template_files(self, mock_read_url_content): tmpl = ''' # comment heat_template_version: 2013-05-23 resources: server1: type: OS::Nova::Server properties: flavor: m1.medium image: cirros user_data_format: RAW user_data: get_file: http://test.example/example ''' expected_files = {u'http://test.example/example': b'echo "test"'} url = 'http://test.example/example' data = b'echo "test"' mock_read_url_content.return_value = data files = api.heat.get_template_files(template_data=tmpl)[0] self.assertEqual(files, expected_files) mock_read_url_content.assert_called_once_with(url) @mock.patch('urllib.request.urlopen') @mock.patch('heatclient.common.utils.read_url_content') def test_get_template_files_with_template_url(self, mock_read_url_content, mock_request): url = 'https://test.example/example.yaml' data = b''' # comment heat_template_version: 2013-05-23 resources: server1: type: OS::Nova::Server properties: flavor: m1.medium image: cirros user_data_format: RAW user_data: get_file: http://test.example/example ''' data2 = b'echo "test"' expected_files = {'http://test.example/example': b'echo "test"'} mock_request.return_value = io.BytesIO(data) mock_read_url_content.return_value = data2 files = api.heat.get_template_files(template_url=url)[0] self.assertEqual(files, expected_files) mock_request.assert_called_once_with(url) mock_read_url_content.assert_called_once_with( 'http://test.example/example') def test_get_template_files_invalid(self): tmpl = ''' # comment heat_template_version: 2013-05-23 resources: server1: type: OS::Nova::Server properties: flavor: m1.medium image: cirros user_data_format: RAW user_data: get_file: file:///example ''' try: api.heat.get_template_files(template_data=tmpl)[0] except exceptions.GetFileError: self.assertRaises(exceptions.GetFileError) def test_template_version_list(self): api_template_versions = self.template_versions.list() heatclient = self.stub_heatclient() heatclient.template_versions = mock.Mock() heatclient.template_versions.list.return_value = api_template_versions template_versions = api.heat.template_version_list(self.request) self.assertCountEqual(template_versions, api_template_versions) heatclient.template_versions.list.assert_called_once_with() def test_template_function_list(self): template_version = self.template_versions.first().version api_template_functions = self.template_functions.list() heatclient = self.stub_heatclient() heatclient.template_versions = mock.Mock() heatclient.template_versions.get.return_value = api_template_functions template_functions = api.heat.template_function_list( self.request, template_version) self.assertCountEqual(template_functions, api_template_functions) heatclient.template_versions.get.assert_called_once_with( template_version) heat-dashboard-3.0.1/heat_dashboard/test/tests/api/__init__.py0000664000175000017500000000000013737340771024340 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/tests/content/0000775000175000017500000000000013737341153023135 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/tests/content/test_stacks.py0000664000175000017500000011275313737340771026054 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import re from unittest import mock from django.conf import settings from django.core import exceptions from django.test.utils import override_settings from django.urls import reverse from django.utils import html from heatclient.common import template_format as hc_format from openstack_dashboard import api as dashboard_api from heat_dashboard import api from heat_dashboard.content.stacks import forms from heat_dashboard.content.stacks import mappings from heat_dashboard.content.stacks import tables from heat_dashboard.test import helpers as test INDEX_TEMPLATE = 'project/stacks/index.html' INDEX_URL = reverse('horizon:project:stacks:index') DETAIL_URL = 'horizon:project:stacks:detail' class MockResource(object): def __init__(self, resource_type, physical_resource_id): self.resource_type = resource_type self.physical_resource_id = physical_resource_id class MappingsTests(test.TestCase): def test_mappings(self): def assertMappingUrl(url, resource_type, physical_resource_id): mock = MockResource(resource_type, physical_resource_id) mock_url = mappings.resource_to_url(mock) self.assertEqual(url, mock_url) assertMappingUrl( '/project/networks/subnets/aaa/detail', 'OS::Neutron::Subnet', 'aaa') assertMappingUrl( None, 'OS::Neutron::Subnet', None) assertMappingUrl( None, None, None) assertMappingUrl( None, 'AWS::AutoScaling::LaunchConfiguration', 'aaa') assertMappingUrl( '/project/instances/aaa/', 'AWS::EC2::Instance', 'aaa') assertMappingUrl( '/project/containers/container/aaa/', 'OS::Swift::Container', 'aaa') assertMappingUrl( None, 'Foo::Bar::Baz', 'aaa') assertMappingUrl( '/project/instances/aaa/', 'OS::Nova::Server', 'aaa') assertMappingUrl( '/project/stacks/stack/aaa/', 'OS::Heat::ResourceGroup', 'aaa') def test_stack_output(self): self.assertEqual(u'
foo
', mappings.stack_output('foo')) self.assertEqual(u'', mappings.stack_output(None)) outputs = ['one', 'two', 'three'] expected_text = """[\n "one",\n "two",\n "three"\n]""" self.assertEqual(u'
%s
' % html.escape(expected_text), mappings.stack_output(outputs)) outputs = {'foo': 'bar'} expected_text = """{\n "foo": "bar"\n}""" self.assertEqual(u'
%s
' % html.escape(expected_text), mappings.stack_output(outputs)) self.assertEqual( u'' 'http://www.example.com/foo', mappings.stack_output('http://www.example.com/foo')) class StackTests(test.TestCase): @override_settings(API_RESULT_PAGE_SIZE=2) @test.create_mocks({api.heat: ('stacks_list',)}) def test_index_paginated(self): stacks = self.stacks.list()[:5] self.mock_stacks_list.side_effect = [ [stacks, True, True], [stacks[:2], True, True], [stacks[2:4], True, True], [stacks[4:], True, True] ] url = reverse('horizon:project:stacks:index') res = self.client.get(url) # get all self.assertEqual(len(res.context['stacks_table'].data), len(stacks)) self.assertTemplateUsed(res, INDEX_TEMPLATE) res = self.client.get(url) # get first page with 2 items self.assertEqual(len(res.context['stacks_table'].data), settings.API_RESULT_PAGE_SIZE) url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'), tables.StacksTable._meta.pagination_param, stacks[2].id) res = self.client.get(url) # get second page (items 2-4) self.assertEqual(len(res.context['stacks_table'].data), settings.API_RESULT_PAGE_SIZE) url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'), tables.StacksTable._meta.pagination_param, stacks[4].id) res = self.client.get(url) # get third page (item 5) self.assertEqual(len(res.context['stacks_table'].data), 1) @override_settings(API_RESULT_PAGE_SIZE=2) @test.create_mocks({api.heat: ('stacks_list',)}) def test_index_prev_paginated(self): stacks = self.stacks.list()[:3] filters = {} self.mock_stacks_list.side_effect = [ [stacks, True, False], [stacks[:2], True, True], [stacks[2:], True, True], [stacks[:2], True, True] ] url = reverse('horizon:project:stacks:index') res = self.client.get(url) # get all self.assertEqual(len(res.context['stacks_table'].data), len(stacks)) self.assertTemplateUsed(res, INDEX_TEMPLATE) res = self.client.get(url) # get first page with 2 items self.assertEqual(len(res.context['stacks_table'].data), settings.API_RESULT_PAGE_SIZE) url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'), tables.StacksTable._meta.pagination_param, stacks[2].id) res = self.client.get(url) # get second page (item 3) self.assertEqual(len(res.context['stacks_table'].data), 1) url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'), tables.StacksTable._meta.prev_pagination_param, stacks[2].id) res = self.client.get(url) # prev back to get first page with 2 pages self.assertEqual(len(res.context['stacks_table'].data), settings.API_RESULT_PAGE_SIZE) self.mock_stacks_list.assert_has_calls([ mock.call(test.IsHttpRequest(), marker=None, paginate=True, sort_dir='desc', filters=filters), mock.call(test.IsHttpRequest(), marker=None, paginate=True, sort_dir='desc', filters=filters), mock.call(test.IsHttpRequest(), marker=stacks[2].id, paginate=True, sort_dir='desc', filters=filters)]) @test.create_mocks({api.heat: ('stack_create', 'template_validate'), dashboard_api.neutron: ('network_list_for_tenant', )}) def test_launch_stack(self): template = self.stack_templates.first() stack = self.stacks.first() self.mock_template_validate.return_value = \ json.loads(template.validate) self.mock_stack_create.reutrn_value = None self.mock_network_list_for_tenant.side_effect = \ [self.networks.list(), self.networks.list()] url = reverse('horizon:project:stacks:select_template') res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/select_template.html') form_data = {'template_source': 'raw', 'template_data': template.data, 'referenced_files': {}, 'method': forms.TemplateForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/create.html') url = reverse('horizon:project:stacks:launch') form_data = {'template_source': 'raw', 'template_data': template.data, 'password': 'password', 'parameters': template.validate, 'stack_name': stack.stack_name, "timeout_mins": 60, "disable_rollback": True, "__param_DBUsername": "admin", "__param_LinuxDistribution": "F17", "__param_InstanceType": "m1.small", "__param_KeyName": "test", "__param_DBPassword": "admin", "__param_DBRootPassword": "admin", "__param_DBName": "wordpress", "__param_Network": self.networks.list()[0]['id'], 'method': forms.CreateStackForm.__name__} res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_template_validate.assert_called_once_with( test.IsHttpRequest(), files={}, template=hc_format.parse(template.data)) self.mock_stack_create.assert_called_once_with( test.IsHttpRequest(), stack_name=stack.stack_name, timeout_mins=60, disable_rollback=True, template=None, parameters=test.IsA(dict), password='password', files=None) self.mock_network_list_for_tenant.assert_has_calls([ mock.call(test.IsHttpRequest(), self.tenant.id), mock.call(test.IsHttpRequest(), self.tenant.id)]) @test.create_mocks({api.heat: ('stack_create', 'template_validate'), dashboard_api.neutron: ('network_list_for_tenant', )}) def test_launch_stack_with_environment(self): template = self.stack_templates.first() environment = self.stack_environments.first() stack = self.stacks.first() self.mock_template_validate.return_value = \ json.loads(template.validate) self.mock_stack_create.return_value = None self.mock_network_list_for_tenant.side_effect = \ [self.networks.list(), self.networks.list()] url = reverse('horizon:project:stacks:select_template') res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/select_template.html') form_data = {'template_source': 'raw', 'template_data': template.data, 'environment_source': 'raw', 'environment_data': environment.data, 'method': forms.TemplateForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/create.html') url = reverse('horizon:project:stacks:launch') form_data = {'template_source': 'raw', 'template_data': template.data, 'environment_source': 'raw', 'environment_data': environment.data, 'password': 'password', 'parameters': template.validate, 'stack_name': stack.stack_name, "timeout_mins": 60, "disable_rollback": True, "__param_DBUsername": "admin", "__param_LinuxDistribution": "F17", "__param_InstanceType": "m1.small", "__param_KeyName": "test", "__param_DBPassword": "admin", "__param_DBRootPassword": "admin", "__param_DBName": "wordpress", "__param_Network": self.networks.list()[0]['id'], 'method': forms.CreateStackForm.__name__} res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_template_validate.assert_called_once_with( test.IsHttpRequest(), files={}, template=hc_format.parse(template.data), environment=environment.data) self.mock_stack_create.assert_called_once_with( test.IsHttpRequest(), stack_name=stack.stack_name, timeout_mins=60, disable_rollback=True, template=None, environment=environment.data, parameters=test.IsA(dict), password='password', files=None) self.mock_network_list_for_tenant.assert_has_calls([ mock.call(test.IsHttpRequest(), self.tenant.id), mock.call(test.IsHttpRequest(), self.tenant.id)]) @test.create_mocks({api.heat: ('template_validate',)}) def test_launch_stack_with_hidden_parameters(self): template = { 'data': ('heat_template_version: 2013-05-23\n' 'parameters:\n' ' public_string:\n' ' type: string\n' ' secret_string:\n' ' type: string\n' ' hidden: true\n'), 'validate': { 'Description': 'No description', 'Parameters': { 'public_string': { 'Label': 'public_string', 'Description': '', 'Type': 'String', 'NoEcho': 'false' }, 'secret_string': { 'Label': 'secret_string', 'Description': '', 'Type': 'String', 'NoEcho': 'true' } } } } self.mock_template_validate.return_value = template['validate'] url = reverse('horizon:project:stacks:select_template') res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/select_template.html') form_data = {'template_source': 'raw', 'template_data': template['data'], 'method': forms.TemplateForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/create.html') # ensure the fields were rendered correctly pattern = ('') secret = ('') self.assertContains(res, pattern, html=True) self.assertContains(res, secret, html=True) self.mock_template_validate.assert_called_once_with( test.IsHttpRequest(), files={}, template=hc_format.parse(template['data'])) @test.create_mocks({api.heat: ('template_validate',)}) def test_launch_stack_with_parameter_group(self): template = { 'data': ('heat_template_version: 2013-05-23\n' 'parameters:\n' ' last_param:\n' ' type: string\n' ' first_param:\n' ' type: string\n' ' middle_param:\n' ' type: string\n' 'parameter_groups:\n' '- parameters:\n' ' - first_param\n' ' - middle_param\n' ' - last_param\n'), 'validate': { 'Description': 'No description', 'Parameters': { 'last_param': { 'Label': 'last_param', 'Description': '', 'Type': 'String', 'NoEcho': 'false' }, 'first_param': { 'Label': 'first_param', 'Description': '', 'Type': 'String', 'NoEcho': 'false' }, 'middle_param': { 'Label': 'middle_param', 'Description': '', 'Type': 'String', 'NoEcho': 'true' } }, 'ParameterGroups': [ { 'parameters': [ 'first_param', 'middle_param', 'last_param' ] } ] } } self.mock_template_validate.return_value = \ template['validate'] url = reverse('horizon:project:stacks:select_template') res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/select_template.html') form_data = {'template_source': 'raw', 'template_data': template['data'], 'method': forms.TemplateForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/create.html') # ensure the fields were rendered in the correct order regex = re.compile('^.*>first_param<.*>middle_param<.*>last_param<.*$', flags=re.DOTALL) self.assertRegex(res.content.decode('utf-8'), regex) @test.create_mocks({api.heat: ('stack_create', 'template_validate')}) def test_launch_stack_parameter_types(self): template = { 'data': ('heat_template_version: 2013-05-23\n' 'parameters:\n' ' param1:\n' ' type: string\n' ' param2:\n' ' type: number\n' ' param3:\n' ' type: json\n' ' param4:\n' ' type: comma_delimited_list\n' ' param5:\n' ' type: boolean\n'), 'validate': { "Description": "No description", "Parameters": { "param1": { "Type": "String", "NoEcho": "false", "Description": "", "Label": "param1" }, "param2": { "Type": "Number", "NoEcho": "false", "Description": "", "Label": "param2" }, "param3": { "Type": "Json", "NoEcho": "false", "Description": "", "Label": "param3" }, "param4": { "Type": "CommaDelimitedList", "NoEcho": "false", "Description": "", "Label": "param4" }, "param5": { "Type": "Boolean", "NoEcho": "false", "Description": "", "Label": "param5" } } } } stack = self.stacks.first() self.mock_template_validate.return_value = \ template['validate'] self.mock_stack_create.return_value = None url = reverse('horizon:project:stacks:select_template') res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/select_template.html') form_data = {'template_source': 'raw', 'template_data': template['data'], 'method': forms.TemplateForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/create.html') # ensure the fields were rendered correctly input_str = ('') self.assertContains(res, input_str.format(3, 'text'), html=True) self.assertContains(res, input_str.format(4, 'text'), html=True) input_str_param2 = ('') self.assertContains(res, input_str_param2, html=True) # post some sample data and make sure it validates url = reverse('horizon:project:stacks:launch') form_data = {'template_source': 'raw', 'template_data': template['data'], 'password': 'password', 'parameters': json.dumps(template['validate']), 'stack_name': stack.stack_name, "timeout_mins": 60, "disable_rollback": True, "__param_param1": "some string", "__param_param2": 42, "__param_param3": '{"key": "value"}', "__param_param4": "a,b,c", "__param_param5": True, 'method': forms.CreateStackForm.__name__} res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_template_validate.assert_called_once_with( test.IsHttpRequest(), files={}, template=hc_format.parse(template['data'])) self.mock_stack_create.assert_called_once_with( test.IsHttpRequest(), stack_name=stack.stack_name, timeout_mins=60, disable_rollback=True, template=hc_format.parse(template['data']), parameters={'param1': 'some string', 'param2': 42, 'param3': '{"key": "value"}', 'param4': 'a,b,c', 'param5': True}, password='password', files={}) @test.create_mocks({api.heat: ('stack_update', 'stack_get', 'template_get', 'template_validate'), dashboard_api.neutron: ('network_list_for_tenant', )}) def test_edit_stack_template(self): template = self.stack_templates.first() stack = self.stacks.first() self.mock_stack_get.return_value = stack self.mock_template_validate.return_value = \ json.loads(template.validate) self.mock_stack_get.reutrn_value = stack self.mock_template_get.return_value = \ json.loads(template.validate) fields = { 'stack_name': stack.stack_name, 'disable_rollback': True, 'timeout_mins': 61, 'password': 'password', 'template': None, 'parameters': test.IsA(dict), 'files': None } self.mock_stack_update.return_value = None self.mock_network_list_for_tenant.return_value = \ self.networks.list() url = reverse('horizon:project:stacks:change_template', args=[stack.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/change_template.html') form_data = {'template_source': 'raw', 'template_data': template.data, 'method': forms.ChangeTemplateForm.__name__} res = self.client.post(url, form_data) url = reverse('horizon:project:stacks:edit_stack', args=[stack.id, ]) form_data = {'template_source': 'raw', 'template_data': template.data, 'password': 'password', 'parameters': template.validate, 'stack_name': stack.stack_name, 'stack_id': stack.id, "timeout_mins": 61, "disable_rollback": True, "__param_DBUsername": "admin", "__param_LinuxDistribution": "F17", "__param_InstanceType": "m1.small", "__param_KeyName": "test", "__param_DBPassword": "admin", "__param_DBRootPassword": "admin", "__param_DBName": "wordpress", "__param_Network": self.networks.list()[0]['id'], 'method': forms.EditStackForm.__name__} res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_template_validate.assert_called_once_with( test.IsHttpRequest(), files={}, template=hc_format.parse(template.data)) self.mock_template_get.assert_called_once_with( test.IsHttpRequest(), stack.id) self.mock_stack_update.assert_called_once_with( test.IsHttpRequest(), stack_id=stack.id, **fields) self.mock_network_list_for_tenant.assert_called_once_with( test.IsHttpRequest(), self.tenant.id) def test_launch_stack_form_invalid_name_digit(self): self._test_launch_stack_invalid_name('2_StartWithDigit') def test_launch_stack_form_invalid_name_underscore(self): self._test_launch_stack_invalid_name('_StartWithUnderscore') def test_launch_stack_form_invalid_name_point(self): self._test_launch_stack_invalid_name('.StartWithPoint') @test.create_mocks({dashboard_api.neutron: ('network_list_for_tenant', )}) def _test_launch_stack_invalid_name(self, name): self.mock_network_list_for_tenant.return_value = \ self.networks.list() template = self.stack_templates.first() url = reverse('horizon:project:stacks:launch') form_data = {'template_source': 'raw', 'template_data': template.data, 'password': 'password', 'parameters': template.validate, 'stack_name': name, "timeout_mins": 60, "disable_rollback": True, "__param_DBUsername": "admin", "__param_LinuxDistribution": "F17", "__param_InstanceType": "m1.small", "__param_KeyName": "test", "__param_DBPassword": "admin", "__param_DBRootPassword": "admin", "__param_DBName": "wordpress", "__param_Network": self.networks.list()[0]['id'], 'method': forms.CreateStackForm.__name__} res = self.client.post(url, form_data) error = ('Name must start with a letter and may only contain letters, ' 'numbers, underscores, periods and hyphens.') self.assertFormErrors(res, 1) self.assertFormError(res, "form", 'stack_name', error) @test.create_mocks({api.heat: ('stacks_list', 'action_check',)}) def test_check_stack(self): stack = self.stacks.first() self.mock_stacks_list.return_value = \ [self.stacks.list(), True, True] self.mock_action_check.return_value = stack form_data = {"action": "stacks__check__%s" % stack.id} res = self.client.post(INDEX_URL, form_data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_mocks({api.heat: ('stacks_list', 'action_suspend',)}) def test_suspend_stack(self): stack = self.stacks.first() self.mock_stacks_list.return_value = \ [self.stacks.list(), True, True] self.mock_action_suspend.return_value = stack form_data = {"action": "stacks__suspend__%s" % stack.id} res = self.client.post(INDEX_URL, form_data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_mocks({api.heat: ('stacks_list', 'action_resume',)}) def test_resume_stack(self): stack = self.stacks.first() self.mock_stacks_list.return_value = \ [self.stacks.list(), True, True] self.mock_action_resume.return_value = stack form_data = {"action": "stacks__resume__%s" % stack.id} res = self.client.post(INDEX_URL, form_data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_mocks({api.heat: ('stack_preview', 'template_validate')}) def test_preview_stack(self): template = self.stack_templates.first() stack = self.stacks.first() self.mock_template_validate.return_value = \ json.loads(template.validate) self.mock_stack_preview.return_value = stack url = reverse('horizon:project:stacks:preview_template') res = self.client.get(url) self.assertTemplateUsed(res, 'project/stacks/preview_template.html') form_data = {'template_source': 'raw', 'template_data': template.data, 'method': forms.PreviewTemplateForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/preview.html') url = reverse('horizon:project:stacks:preview') form_data = {'template_source': 'raw', 'template_data': template.data, 'parameters': template.validate, 'stack_name': stack.stack_name, "timeout_mins": 60, "disable_rollback": True, "__param_DBUsername": "admin", "__param_LinuxDistribution": "F17", "__param_InstanceType": "m1.small", "__param_KeyName": "test", "__param_DBPassword": "admin", "__param_DBRootPassword": "admin", "__param_DBName": "wordpress", 'method': forms.PreviewStackForm.__name__} res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'project/stacks/preview_details.html') self.assertEqual(res.context['stack_preview']['stack_name'], stack.stack_name) self.mock_template_validate.assert_called_once_with( test.IsHttpRequest(), files={}, template=hc_format.parse(template.data)) @test.create_mocks({api.heat: ('stack_get', 'template_get', 'resources_list')}) def test_detail_stack_topology(self): stack = self.stacks.first() template = self.stack_templates.first() self.mock_stack_get.return_value = stack self.mock_template_get.return_value = \ json.loads(template.validate) self.mock_resources_list.return_value = [] url = '?'.join([reverse(DETAIL_URL, args=[stack.id]), '='.join(['tab', 'stack_details__stack_topology'])]) res = self.client.get(url) tab = res.context['tab_group'].get_tab('topology') d3_data = tab.data['d3_data'] self.assertEqual(tab.template_name, 'project/stacks/_detail_topology.html') # status is CREATE_COMPLETE, so we expect the topology to display it self.assertIn('info_box', d3_data) self.assertIn('stack-green.svg', d3_data) self.assertIn('Create Complete', d3_data) self.mock_template_get.assert_called_once_with( test.IsHttpRequest(), stack.id) self.mock_resources_list.assert_called_once_with( test.IsHttpRequest(), stack.stack_name) @test.create_mocks({api.heat: ('stack_get', 'template_get', 'resources_list')}) def test_detail_stack_overview(self): stack = self.stacks.first() template = self.stack_templates.first() self.mock_stack_get.return_value = stack self.mock_resources_list.return_value = [] self.mock_template_get.return_value = \ json.loads(template.validate) url = '?'.join([reverse(DETAIL_URL, args=[stack.id]), '='.join(['tab', 'stack_details__stack_overview'])]) res = self.client.get(url) tab = res.context['tab_group'].get_tab('overview') overview_data = tab.data['stack'] self.assertEqual(tab.template_name, 'project/stacks/_detail_overview.html') self.assertEqual(stack.stack_name, overview_data.stack_name) self.mock_template_get.assert_called_once_with( test.IsHttpRequest(), stack.id) @test.create_mocks({api.heat: ('stack_get', 'template_get', 'resources_list')}) def test_detail_stack_resources(self): stack = self.stacks.first() template = self.stack_templates.first() self.mock_stack_get.return_value = stack self.mock_resources_list.return_value = [] self.mock_template_get.return_value = \ json.loads(template.validate) url = '?'.join([reverse(DETAIL_URL, args=[stack.id]), '='.join(['tab', 'stack_details__resource_overview'])]) res = self.client.get(url) tab = res.context['tab_group'].get_tab('resources') self.assertEqual(tab.template_name, 'project/stacks/_detail_resources.html') @test.create_mocks({api.heat: ('stack_get', 'template_get')}) def test_detail_stack_template(self): stack = self.stacks.first() template = self.stack_templates.first() self.mock_stack_get.return_value = stack self.mock_template_get.return_value = \ json.loads(template.validate) url = '?'.join([reverse(DETAIL_URL, args=[stack.id]), '='.join(['tab', 'stack_details__stack_template'])]) res = self.client.get(url) tab = res.context['tab_group'].get_tab('stack_template') template_data = tab.data['stack_template'] self.assertEqual(tab.template_name, 'project/stacks/_stack_template.html') self.assertIn(json.loads(template.validate)['Description'], template_data) self.mock_stack_get.assert_called_once_with( test.IsHttpRequest(), stack.id) self.mock_template_get.assert_called_once_with( test.IsHttpRequest(), stack.id) @test.create_mocks({api.heat: ('resource_get', 'resource_metadata_get')}) def test_resource_view(self): stack = self.stacks.first() resource = self.heat_resources.first() metadata = {} self.mock_resource_get.return_value = resource self.mock_resource_metadata_get.return_value = metadata url = reverse('horizon:project:stacks:resource', args=[stack.id, resource.resource_name]) res = self.client.get(url) self.assertTemplateUsed(res, 'horizon/common/_detail.html') self.assertTemplateUsed(res, 'project/stacks/_resource_overview.html') self.assertEqual(res.context['resource'].logical_resource_id, resource.logical_resource_id) self.mock_resource_get.assert_called_once_with( test.IsHttpRequest(), stack.id, resource.resource_name) self.mock_resource_get.assert_called_once_with( test.IsHttpRequest(), stack.id, resource.resource_name) class TemplateFormTests(test.TestCase): class SimpleFile(object): def __init__(self, name, data): self.name = name self.data = data def read(self): return self.data def test_create_upload_form_attributes(self): attrs = forms.create_upload_form_attributes( 'env', 'url', 'Environment') self.assertEqual(attrs['data-envsource-url'], 'Environment') def test_clean_file_upload_form_url(self): kwargs = {'next_view': 'Launch Stack'} t = forms.TemplateForm({}, **kwargs) precleaned = { 'template_url': 'http://templateurl.com', } t.clean_uploaded_files('template', 'template', precleaned, {}) self.assertEqual(precleaned['template_url'], 'http://templateurl.com') def test_clean_file_upload_form_multiple(self): kwargs = {'next_view': 'Launch Stack'} t = forms.TemplateForm({}, **kwargs) precleaned = { 'template_url': 'http://templateurl.com', 'template_data': 'http://templateurl.com', } self.assertRaises( exceptions.ValidationError, t.clean_uploaded_files, 'template', 'template', precleaned, {}) def test_clean_file_upload_form_invalid_json(self): kwargs = {'next_view': 'Launch Stack'} t = forms.TemplateForm({}, **kwargs) precleaned = { 'template_data': 'http://templateurl.com', } json_str = '{notvalidjson::::::json/////json' files = {'template_upload': self.SimpleFile('template_name', json_str.encode('utf-8'))} self.assertRaises( exceptions.ValidationError, t.clean_uploaded_files, 'template', 'template', precleaned, files) def test_clean_file_upload_form_valid_data(self): kwargs = {'next_view': 'Launch Stack'} t = forms.TemplateForm({}, **kwargs) precleaned = { 'template_data': 'http://templateurl.com', } json_str = '{"isvalid":"json"}' files = {'template_upload': self.SimpleFile('template_name', json_str.encode('utf-8'))} t.clean_uploaded_files('template', 'template', precleaned, files) self.assertEqual( json_str, precleaned['template_data']) heat-dashboard-3.0.1/heat_dashboard/test/tests/content/test_template_generator.py0000664000175000017500000001225413737340771030440 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json from django.urls import reverse from openstack_dashboard import api as dashboard_api from heat_dashboard import api from heat_dashboard.test import helpers as test class TemplateGeneratorTests(test.TestCase): def test_index(self): self.client.get(reverse('horizon:project:template_generator:index')) self.assertTemplateUsed( template_name='project/template_generator/index.html') @test.create_mocks({ api.heat: ('template_version_list', ), dashboard_api.neutron: ( 'network_list', 'subnet_list', 'tenant_floating_ip_list', 'port_list', 'security_group_list', 'router_list', 'policy_list'), dashboard_api.cinder: ( 'volume_list', 'volume_snapshot_list', 'volume_type_list', 'volume_backup_list'), dashboard_api.glance: ('image_list_detailed', ), dashboard_api.nova: ('availability_zone_list', 'flavor_list', 'server_list', 'keypair_list')}) def test_option(self): volumes = self.cinder_volumes.list() volume_snapshots = self.cinder_volume_snapshots.list() volume_types = self.cinder_volume_types.list() volume_backups = self.cinder_volume_backups.list() images = self.imagesV2.list() networks = self.networks.list() subnets = self.subnets.list() floating_ips = self.floating_ips.list() ports = self.ports.list() security_groups = self.security_groups.list() routers = self.routers.list() qos_policies = self.qos_policies.list() availability_zones = self.availability_zones.list() flavors = self.flavors.list() instances = self.servers.list() keypairs = self.keypairs.list() template_versions = self.template_versions.list() self.mock_volume_list.return_value = volumes self.mock_volume_snapshot_list.return_value = volume_snapshots self.mock_volume_type_list.return_vlue = volume_types self.mock_volume_backup_list.return_value = volume_backups self.mock_image_list_detailed.return_value = images self.mock_network_list.return_value = networks self.mock_subnet_list.return_value = subnets self.mock_tenant_floating_ip_list.return_value = floating_ips self.mock_port_list.return_value = ports self.mock_security_group_list.return_value = security_groups self.mock_router_list.return_value = routers self.mock_policy_list.return_value = qos_policies self.mock_availability_zone_list.return_value = availability_zones self.mock_flavor_list.return_value = flavors self.mock_server_list.return_value = instances self.mock_keypair_list.return_value = keypairs self.mock_template_version_list.return_value = template_versions resp = self.client.get(reverse( 'horizon:project:template_generator:apis')) data = resp.content if isinstance(data, bytes): data = data.decode('utf-8') json_data = json.loads(data) self.assertEqual(len(json_data.keys()), 20) self.mock_volume_list.assert_called_once_with( test.IsHttpRequest()) self.mock_volume_snapshot_list.assert_called_once_with( test.IsHttpRequest()) self.mock_volume_type_list.assert_called_once_with( test.IsHttpRequest()) self.mock_volume_backup_list.assert_called_once_with( test.IsHttpRequest()) self.mock_image_list_detailed.assert_called_once_with( test.IsHttpRequest()) self.mock_network_list.assert_called_once_with( test.IsHttpRequest()) self.mock_subnet_list.assert_called_once_with( test.IsHttpRequest()) self.mock_tenant_floating_ip_list.assert_called_once_with( test.IsHttpRequest(), True) self.mock_port_list.assert_called_once_with( test.IsHttpRequest()) self.mock_security_group_list.assert_called_once_with( test.IsHttpRequest()) self.mock_router_list.assert_called_once_with( test.IsHttpRequest()) self.mock_policy_list.assert_called_once_with( test.IsHttpRequest()) self.mock_availability_zone_list.assert_called_once_with( test.IsHttpRequest()) self.mock_flavor_list.assert_called_once_with( test.IsHttpRequest()) self.mock_server_list.assert_called_once_with( test.IsHttpRequest()) self.mock_keypair_list.assert_called_once_with( test.IsHttpRequest()) self.mock_template_version_list.assert_called_once_with( test.IsHttpRequest()) heat-dashboard-3.0.1/heat_dashboard/test/tests/content/test_resource_types.py0000664000175000017500000000346113737340771027632 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from heat_dashboard import api from heat_dashboard.test import helpers as test from heat_dashboard.test.helpers import IsHttpRequest class ResourceTypesTests(test.TestCase): @test.create_mocks({api.heat: ('resource_types_list',)}) def test_index(self): self.mock_resource_types_list.return_value = \ self.resource_types.list() res = self.client.get( reverse('horizon:project:resource_types:index')) self.assertTemplateUsed( res, 'horizon/common/_data_table_view.html') self.assertContains(res, 'AWS::CloudFormation::Stack') self.mock_resource_types_list.assert_called_once_with( IsHttpRequest(), filters={}) @test.create_mocks({api.heat: ('resource_type_get',)}) def test_detail_view(self): rt = self.api_resource_types.first() self.mock_resource_type_get.return_value = rt url = reverse('horizon:project:resource_types:details', args=[rt['resource_type']]) res = self.client.get(url) self.assertTemplateUsed(res, 'horizon/common/_detail.html') self.assertNoMessages() self.mock_resource_type_get.assert_called_once_with( IsHttpRequest(), rt['resource_type']) heat-dashboard-3.0.1/heat_dashboard/test/tests/content/__init__.py0000664000175000017500000000000013737340771025241 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/tests/content/test_template_versions.py0000664000175000017500000000612713737340771030324 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from heat_dashboard import api from heat_dashboard.test import helpers as test from heat_dashboard.test.helpers import IsHttpRequest class TemplateVersionsTests(test.TestCase): INDEX_URL = reverse('horizon:project:template_versions:index') @test.create_mocks({api.heat: ('template_version_list',)}) def test_index(self): self.mock_template_version_list.return_value = \ self.template_versions.list() res = self.client.get(self.INDEX_URL) self.mock_template_version_list.assert_called_once_with( IsHttpRequest()) self.assertTemplateUsed( res, 'project/template_versions/index.html') self.assertContains(res, 'HeatTemplateFormatVersion.2012-12-12') @test.create_mocks({api.heat: ('template_version_list',)}) def test_index_exception(self): self.mock_template_version_list.side_effect = \ self.exceptions.heat res = self.client.get(self.INDEX_URL) self.mock_template_version_list.assert_called_once_with( IsHttpRequest()) self.assertTemplateUsed( res, 'project/template_versions/index.html') self.assertEqual(len(res.context['table'].data), 0) self.assertMessageCount(res, error=1) @test.create_mocks({api.heat: ('template_function_list',)}) def test_detail_view(self): t_version = self.template_versions.first().version t_functions = self.template_functions.list() self.mock_template_function_list.return_value = t_functions url = reverse('horizon:project:template_versions:details', args=[t_version]) res = self.client.get(url) self.mock_template_function_list.assert_called_once_with( IsHttpRequest(), t_version) self.assertTemplateUsed(res, 'horizon/common/_detail.html') self.assertNoMessages() @test.create_mocks({api.heat: ('template_function_list',)}) def test_detail_view_with_exception(self): t_version = self.template_versions.first().version self.mock_template_function_list.side_effect = \ self.exceptions.heat url = reverse('horizon:project:template_versions:details', args=[t_version]) res = self.client.get(url) self.mock_template_function_list.assert_called_once_with( IsHttpRequest(), t_version) self.assertTemplateUsed(res, 'horizon/common/_detail.html') self.assertEqual(len(res.context['table'].data), 0) self.assertMessageCount(res, error=1) heat-dashboard-3.0.1/heat_dashboard/test/test_data/0000775000175000017500000000000013737341153022271 5ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/test_data/cinder_data.py0000664000175000017500000002061613737340771025112 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.v2 import volume_backups as vol_backups from cinderclient.v2 import volume_snapshots as vol_snaps from cinderclient.v2 import volume_types from cinderclient.v2 import volumes from openstack_dashboard import api from openstack_dashboard.test.test_data import utils def data(TEST): TEST.cinder_volumes = utils.TestDataContainer() TEST.cinder_volume_backups = utils.TestDataContainer() TEST.cinder_volume_types = utils.TestDataContainer() TEST.cinder_volume_snapshots = utils.TestDataContainer() # Volumes - Cinder v1 volume = volumes.Volume( volumes.VolumeManager(None), {'id': "11023e92-8008-4c8b-8059-7f2293ff3887", 'status': 'available', 'size': 40, 'display_name': 'Volume name', 'display_description': 'Volume description', 'created_at': '2014-01-27 10:30:00', 'volume_type': None, 'attachments': []}) nameless_volume = volumes.Volume( volumes.VolumeManager(None), {"id": "4b069dd0-6eaa-4272-8abc-5448a68f1cce", "status": 'available', "size": 10, "display_name": '', "display_description": '', "device": "/dev/hda", "created_at": '2010-11-21 18:34:25', "volume_type": 'vol_type_1', "attachments": []}) other_volume = volumes.Volume( volumes.VolumeManager(None), {'id': "21023e92-8008-1234-8059-7f2293ff3889", 'status': 'in-use', 'size': 10, 'display_name': u'my_volume', 'display_description': '', 'created_at': '2013-04-01 10:30:00', 'volume_type': None, 'attachments': [{"id": "1", "server_id": '1', "device": "/dev/hda"}]}) volume_with_type = volumes.Volume( volumes.VolumeManager(None), {'id': "7dcb47fd-07d9-42c2-9647-be5eab799ebe", 'name': 'my_volume2', 'status': 'in-use', 'size': 10, 'display_name': u'my_volume2', 'display_description': '', 'created_at': '2013-04-01 10:30:00', 'volume_type': 'vol_type_2', 'attachments': [{"id": "2", "server_id": '2', "device": "/dev/hdb"}]}) non_bootable_volume = volumes.Volume( volumes.VolumeManager(None), {'id': "21023e92-8008-1234-8059-7f2293ff3890", 'status': 'in-use', 'size': 10, 'display_name': u'my_volume', 'display_description': '', 'created_at': '2013-04-01 10:30:00', 'volume_type': None, 'bootable': False, 'attachments': [{"id": "1", "server_id": '1', "device": "/dev/hda"}]}) volume.bootable = 'true' nameless_volume.bootable = 'true' other_volume.bootable = 'true' TEST.cinder_volumes.add(api.cinder.Volume(volume)) TEST.cinder_volumes.add(api.cinder.Volume(nameless_volume)) TEST.cinder_volumes.add(api.cinder.Volume(other_volume)) TEST.cinder_volumes.add(api.cinder.Volume(volume_with_type)) TEST.cinder_volumes.add(api.cinder.Volume(non_bootable_volume)) vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), {'id': u'1', 'name': u'vol_type_1', 'description': 'type 1 description', 'extra_specs': {'foo': 'bar', 'volume_backend_name': 'backend_1'}}) vol_type2 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), {'id': u'2', 'name': u'vol_type_2', 'description': 'type 2 description'}) vol_type3 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), {'id': u'3', 'name': u'vol_type_3', 'is_public': False, 'description': 'type 3 description'}) TEST.cinder_volume_types.add(vol_type1, vol_type2, vol_type3) # Volumes - Cinder v2 volume_v2 = volumes.Volume( volumes.VolumeManager(None), {'id': "31023e92-8008-4c8b-8059-7f2293ff1234", 'name': 'v2_volume', 'description': "v2 Volume Description", 'status': 'available', 'size': 20, 'created_at': '2014-01-27 10:30:00', 'volume_type': None, 'os-vol-host-attr:host': 'host@backend-name#pool', 'bootable': 'true', 'attachments': []}) volume_v2.bootable = 'true' TEST.cinder_volumes.add(api.cinder.Volume(volume_v2)) snapshot = vol_snaps.Snapshot( vol_snaps.SnapshotManager(None), {'id': '5f3d1c33-7d00-4511-99df-a2def31f3b5d', 'display_name': 'test snapshot', 'display_description': 'volume snapshot', 'size': 40, 'status': 'available', 'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'}) snapshot2 = vol_snaps.Snapshot( vol_snaps.SnapshotManager(None), {'id': 'c9d0881a-4c0b-4158-a212-ad27e11c2b0f', 'name': '', 'description': 'v2 volume snapshot description', 'size': 80, 'status': 'available', 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) snapshot3 = vol_snaps.Snapshot( vol_snaps.SnapshotManager(None), {'id': 'c9d0881a-4c0b-4158-a212-ad27e11c2b0e', 'name': '', 'description': 'v2 volume snapshot description 2', 'size': 80, 'status': 'available', 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) snapshot4 = vol_snaps.Snapshot( vol_snaps.SnapshotManager(None), {'id': 'cd6be1eb-82ca-4587-8036-13c37c00c2b1', 'name': '', 'description': 'v2 volume snapshot with metadata description', 'size': 80, 'status': 'available', 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234', 'metadata': {'snapshot_meta_key': 'snapshot_meta_value'}}) snapshot.bootable = 'true' snapshot2.bootable = 'true' TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot)) TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot2)) TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot3)) TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot4)) TEST.cinder_volume_snapshots.first()._volume = volume volume_backup1 = vol_backups.VolumeBackup( vol_backups.VolumeBackupManager(None), {'id': 'a374cbb8-3f99-4c3f-a2ef-3edbec842e31', 'name': 'backup1', 'description': 'volume backup 1', 'size': 10, 'status': 'available', 'container_name': 'volumebackups', 'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'}) volume_backup2 = vol_backups.VolumeBackup( vol_backups.VolumeBackupManager(None), {'id': 'c321cbb8-3f99-4c3f-a2ef-3edbec842e52', 'name': 'backup2', 'description': 'volume backup 2', 'size': 20, 'status': 'available', 'container_name': 'volumebackups', 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) volume_backup3 = vol_backups.VolumeBackup( vol_backups.VolumeBackupManager(None), {'id': 'c321cbb8-3f99-4c3f-a2ef-3edbec842e53', 'name': 'backup3', 'description': 'volume backup 3', 'size': 20, 'status': 'available', 'container_name': 'volumebackups', 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) TEST.cinder_volume_backups.add(volume_backup1) TEST.cinder_volume_backups.add(volume_backup2) TEST.cinder_volume_backups.add(volume_backup3) heat-dashboard-3.0.1/heat_dashboard/test/test_data/utils.py0000664000175000017500000001032713737340771024013 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. def load_test_data(load_onto=None): from heat_dashboard.test.test_data import cinder_data from heat_dashboard.test.test_data import exceptions from heat_dashboard.test.test_data import glance_data from heat_dashboard.test.test_data import heat_data from heat_dashboard.test.test_data import keystone_data from heat_dashboard.test.test_data import neutron_data from heat_dashboard.test.test_data import nova_data # The order of these loaders matters, some depend on others. loaders = ( exceptions.data, keystone_data.data, glance_data.data, nova_data.data, cinder_data.data, neutron_data.data, # swift_data.data, heat_data.data, ) if load_onto: for data_func in loaders: data_func(load_onto) return load_onto else: return TestData(*loaders) class TestData(object): """Holder object for test data. Any functions passed to the init method will be called with the ``TestData`` object as their only argument. They can then load data onto the object as desired. The idea is to use the instantiated object like this:: >>> import glance_data >>> TEST = TestData(glance_data.data) >>> TEST.images.list() [, ] >>> TEST.images.first() You can load as little or as much data as you like as long as the loaders don't conflict with each other. See the :class:`~openstack_dashboard.test.test_data.utils.TestDataContainer` class for a list of available methods. """ def __init__(self, *args): for data_func in args: data_func(self) class TestDataContainer(object): """A container for test data objects. The behavior of this class is meant to mimic a "manager" class, which has convenient shortcuts for common actions like "list", "filter", "get", and "add". """ def __init__(self): self._objects = [] def add(self, *args): """Add a new object to this container. Generally this method should only be used during data loading, since adding data during a test can affect the results of other tests. """ for obj in args: if obj not in self._objects: self._objects.append(obj) def list(self): """Returns a list of all objects in this container.""" return self._objects def filter(self, filtered=None, **kwargs): """Returns objects whose attributes match the given kwargs.""" if filtered is None: filtered = self._objects try: key, value = kwargs.popitem() except KeyError: # We're out of filters, return return filtered def get_match(obj): return hasattr(obj, key) and getattr(obj, key) == value filtered = [obj for obj in filtered if get_match(obj)] return self.filter(filtered=filtered, **kwargs) def get(self, **kwargs): """Returns a single object whose attributes match the given kwargs. An error will be raised if the arguments provided don't return exactly one match. """ matches = self.filter(**kwargs) if not matches: raise Exception("No matches found.") elif len(matches) > 1: raise Exception("Multiple matches found.") else: return matches.pop() def first(self): """Returns the first object from this container.""" return self._objects[0] def count(self): return len(self._objects) heat-dashboard-3.0.1/heat_dashboard/test/test_data/__init__.py0000664000175000017500000000000013737340771024375 0ustar zuulzuul00000000000000heat-dashboard-3.0.1/heat_dashboard/test/test_data/neutron_data.py0000664000175000017500000001730513737340771025341 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy from openstack_dashboard.api import neutron from heat_dashboard.test.test_data import utils def data(TEST): # Data returned by openstack_dashboard.api.neutron wrapper. TEST.networks = utils.TestDataContainer() TEST.subnets = utils.TestDataContainer() TEST.ports = utils.TestDataContainer() TEST.routers = utils.TestDataContainer() TEST.floating_ips = utils.TestDataContainer() TEST.security_groups = utils.TestDataContainer() TEST.qos_policies = utils.TestDataContainer() # Data return by neutronclient. TEST.api_networks = utils.TestDataContainer() TEST.api_subnets = utils.TestDataContainer() # 1st network. network_dict = {'admin_state_up': True, 'id': '82288d84-e0a5-42ac-95be-e6af08727e42', 'name': 'net1', 'status': 'ACTIVE', 'subnets': ['e8abc972-eb0c-41f1-9edd-4bc6e3bcd8c9', '41e53a49-442b-4307-9e9a-88967a6b6657'], 'tenant_id': '1', 'router:external': False, 'shared': False} subnet_dict = {'allocation_pools': [{'end': '10.0.0.254', 'start': '10.0.0.2'}], 'dns_nameservers': [], 'host_routes': [], 'cidr': '10.0.0.0/24', 'enable_dhcp': True, 'gateway_ip': '10.0.0.1', 'id': network_dict['subnets'][0], 'ip_version': 4, 'name': 'mysubnet1', 'network_id': network_dict['id'], 'tenant_id': network_dict['tenant_id']} subnetv6_dict = { 'allocation_pools': [{'start': 'fdb6:b88a:488e::2', 'end': 'fdb6:b88a:488e:0:ffff:ffff:ffff:ffff'}], 'dns_nameservers': [], 'host_routes': [], 'cidr': 'fdb6:b88a:488e::/64', 'enable_dhcp': True, 'gateway_ip': 'fdb6:b88a:488e::1', 'id': network_dict['subnets'][1], 'ip_version': 6, 'name': 'myv6subnet', 'network_id': network_dict['id'], 'tenant_id': network_dict['tenant_id'], 'ipv6_ra_mode': 'slaac', 'ipv6_address_mode': 'slaac' } TEST.api_networks.add(network_dict) TEST.api_subnets.add(subnet_dict) TEST.api_subnets.add(subnetv6_dict) network = copy.deepcopy(network_dict) subnet = neutron.Subnet(subnet_dict) subnetv6 = neutron.Subnet(subnetv6_dict) network['subnets'] = [subnet, subnetv6] TEST.networks.add(neutron.Network(network)) TEST.subnets.add(subnet) TEST.subnets.add(subnetv6) # Ports on 1st network. port_dict = { 'admin_state_up': True, 'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890', 'device_owner': 'network:dhcp', 'fixed_ips': [{'ip_address': '10.0.0.3', 'subnet_id': subnet_dict['id']}], 'id': '063cf7f3-ded1-4297-bc4c-31eae876cc91', 'mac_address': 'fa:16:3e:9c:d5:7e', 'name': '', 'network_id': network_dict['id'], 'status': 'ACTIVE', 'tenant_id': network_dict['tenant_id'], 'binding:vnic_type': 'normal', 'binding:host_id': 'host', 'allowed_address_pairs': [ {'ip_address': '174.0.0.201', 'mac_address': 'fa:16:3e:7a:7b:18'} ], 'port_security_enabled': True, 'security_groups': [], } TEST.ports.add(neutron.Port(port_dict)) # External network. network_dict = {'admin_state_up': True, 'id': '9b466b94-213a-4cda-badf-72c102a874da', 'name': 'ext_net', 'status': 'ACTIVE', 'subnets': ['d6bdc71c-7566-4d32-b3ff-36441ce746e8'], 'tenant_id': '3', 'router:external': True, 'shared': False} subnet_dict = {'allocation_pools': [{'start': '172.24.4.226.', 'end': '172.24.4.238'}], 'dns_nameservers': [], 'host_routes': [], 'cidr': '172.24.4.0/28', 'enable_dhcp': False, 'gateway_ip': '172.24.4.225', 'id': 'd6bdc71c-7566-4d32-b3ff-36441ce746e8', 'ip_version': 4, 'name': 'ext_subnet', 'network_id': network_dict['id'], 'tenant_id': network_dict['tenant_id']} ext_net = network_dict network = copy.deepcopy(network_dict) subnet = neutron.Subnet(subnet_dict) network['subnets'] = [subnet] TEST.networks.add(neutron.Network(network)) TEST.subnets.add(subnet) assoc_port = port_dict router_dict = {'id': '279989f7-54bb-41d9-ba42-0d61f12fda61', 'name': 'router1', 'status': 'ACTIVE', 'admin_state_up': True, 'distributed': True, 'external_gateway_info': {'network_id': ext_net['id']}, 'tenant_id': '1', 'availability_zone_hints': ['nova']} TEST.routers.add(neutron.Router(router_dict)) # Associated (with compute port on 1st network). fip_dict = {'tenant_id': '1', 'floating_ip_address': '172.16.88.228', 'floating_network_id': ext_net['id'], 'id': 'a97af8f2-3149-4b97-abbd-e49ad19510f7', 'fixed_ip_address': assoc_port['fixed_ips'][0]['ip_address'], 'port_id': assoc_port['id'], 'router_id': router_dict['id']} fip_with_instance = copy.deepcopy(fip_dict) fip_with_instance.update({'instance_id': '1', 'instance_type': 'compute'}) TEST.floating_ips.add(neutron.FloatingIp(fip_with_instance)) # Security group. sec_group_1 = {'tenant_id': '1', 'description': 'default', 'id': 'faad7c80-3b62-4440-967c-13808c37131d', 'name': 'default'} sec_group_2 = {'tenant_id': '1', 'description': 'NotDefault', 'id': '27a5c9a1-bdbb-48ac-833a-2e4b5f54b31d', 'name': 'other_group'} sec_group_3 = {'tenant_id': '1', 'description': 'NotDefault', 'id': '443a4d7a-4bd2-4474-9a77-02b35c9f8c95', 'name': 'another_group'} groups = [sec_group_1, sec_group_2, sec_group_3] sg_name_dict = dict([(sg['id'], sg['name']) for sg in groups]) for sg in groups: sg['security_group_rules'] = [] # OpenStack Dashboard internaly API. TEST.security_groups.add( neutron.SecurityGroup(copy.deepcopy(sg), sg_name_dict)) # qos policies policy_dict = {'id': 'a21dcd22-7189-cccc-aa32-22adafaf16a7', 'name': 'policy 1', 'tenant_id': '1'} TEST.qos_policies.add(neutron.QoSPolicy(policy_dict)) policy_dict1 = {'id': 'a21dcd22-7189-ssss-aa32-22adafaf16a7', 'name': 'policy 2', 'tenant_id': '1'} TEST.qos_policies.add(neutron.QoSPolicy(policy_dict1)) heat-dashboard-3.0.1/heat_dashboard/test/test_data/exceptions.py0000664000175000017500000000325013737340771025031 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import heatclient.exc as heat_exceptions from heat_dashboard.test.test_data import utils def create_stubbed_exception(cls, status_code=500): msg = "Expected failure." def fake_init_exception(self, code=None, message=None, **kwargs): if code is not None: if hasattr(self, 'http_status'): self.http_status = code else: self.code = code self.message = message or self.__class__.message try: # Neutron sometimes updates the message with additional # information, like a reason. self.message = self.message % kwargs except Exception: pass # We still have the main error message. def fake_str(self): return str(self.message) cls.__init__ = fake_init_exception cls.__str__ = fake_str cls.silence_logging = True return cls(status_code, msg) def data(TEST): TEST.exceptions = utils.TestDataContainer() heat_exception = heat_exceptions.HTTPException TEST.exceptions.heat = create_stubbed_exception(heat_exception) heat-dashboard-3.0.1/heat_dashboard/test/test_data/glance_data.py0000664000175000017500000003713013737340771025076 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from glanceclient.v1 import images from openstack_dashboard import api from heat_dashboard.test.test_data import utils class Namespace(dict): def __repr__(self): return "" % self._info def __init__(self, info): super(Namespace, self).__init__() self.__dict__.update(info) self.update(info) self._info = info def as_json(self, indent=4): return self.__dict__ class APIResourceV2(dict): _base_props = [ 'id', 'name', 'status', 'visibility', 'protected', 'checksum', 'owner', 'size', 'virtual_size', 'container_format', 'disk_format', 'created_at', 'updated_at', 'tags', 'direct_url', 'min_ram', 'min_disk', 'self', 'file', 'schema', 'locations'] def __getattr__(self, item): if item == 'schema': return {'properties': {k: '' for k in self._base_props}} else: return self.get(item) def data(TEST): TEST.images = utils.TestDataContainer() TEST.images_api = utils.TestDataContainer() TEST.snapshots = utils.TestDataContainer() TEST.metadata_defs = utils.TestDataContainer() TEST.imagesV2 = utils.TestDataContainer() # Snapshots snapshot_dict = {'name': u'snapshot', 'container_format': u'ami', 'id': 3, 'status': "active", 'owner': TEST.tenant.id, 'properties': {'image_type': u'snapshot'}, 'is_public': False, 'protected': False} snapshot_dict_no_owner = {'name': u'snapshot 2', 'container_format': u'ami', 'id': 4, 'status': "active", 'owner': None, 'properties': {'image_type': u'snapshot'}, 'is_public': False, 'protected': False} snapshot_dict_queued = {'name': u'snapshot 2', 'container_format': u'ami', 'id': 5, 'status': "queued", 'owner': TEST.tenant.id, 'properties': {'image_type': u'snapshot'}, 'is_public': False, 'protected': False} snapshot = images.Image(images.ImageManager(None), snapshot_dict) TEST.snapshots.add(api.glance.Image(snapshot)) snapshot = images.Image(images.ImageManager(None), snapshot_dict_no_owner) TEST.snapshots.add(api.glance.Image(snapshot)) snapshot = images.Image(images.ImageManager(None), snapshot_dict_queued) TEST.snapshots.add(api.glance.Image(snapshot)) # Images image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822', 'name': 'public_image', 'disk_format': u'qcow2', 'status': "active", 'size': 20 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': TEST.tenant.id, 'container_format': 'novaImage', 'properties': {'image_type': u'image'}, 'is_public': True, 'protected': False, 'min_ram': 0, 'created_at': '2014-02-14T20:56:53'} public_image = images.Image(images.ImageManager(None), image_dict) image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe', 'name': 'private_image', 'status': "active", 'size': 10 * 1024 ** 2, 'virtual_size': 20 * 1024 ** 2, 'min_disk': 0, 'owner': TEST.tenant.id, 'container_format': 'aki', 'is_public': False, 'protected': False, 'min_ram': 0, 'created_at': '2014-03-14T12:56:53'} private_image = images.Image(images.ImageManager(None), image_dict) image_dict = {'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c', 'name': 'protected_images', 'status': "active", 'owner': TEST.tenant.id, 'size': 2 * 1024 ** 3, 'virtual_size': None, 'min_disk': 30, 'container_format': 'novaImage', 'properties': {'image_type': u'image'}, 'is_public': True, 'protected': True, 'min_ram': 0, 'created_at': '2014-03-16T06:22:14'} protected_image = images.Image(images.ImageManager(None), image_dict) image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32', 'name': None, 'status': "active", 'size': 5 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': TEST.tenant.id, 'container_format': 'novaImage', 'properties': {'image_type': u'image'}, 'is_public': True, 'protected': False, 'min_ram': 0} public_image2 = images.Image(images.ImageManager(None), image_dict) image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10', 'name': 'private_image 2', 'status': "active", 'size': 30 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': TEST.tenant.id, 'container_format': 'aki', 'is_public': False, 'protected': False, 'min_ram': 0} private_image2 = images.Image(images.ImageManager(None), image_dict) image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132', 'name': 'private_image 3', 'status': "active", 'size': 2 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': TEST.tenant.id, 'container_format': 'aki', 'is_public': False, 'protected': False, 'min_ram': 0} private_image3 = images.Image(images.ImageManager(None), image_dict) # A shared image. Not public and not local tenant. image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849', 'name': 'shared_image 1', 'status': "active", 'size': 8 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': 'someothertenant', 'container_format': 'aki', 'is_public': False, 'protected': False, 'min_ram': 0} shared_image1 = images.Image(images.ImageManager(None), image_dict) # "Official" image. Public and tenant matches an entry # in IMAGES_LIST_FILTER_TENANTS. image_dict = {'id': 'f448704f-0ce5-4d34-8441-11b6581c6619', 'name': 'official_image 1', 'status': "active", 'size': 2 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': 'officialtenant', 'container_format': 'aki', 'is_public': True, 'protected': False, 'min_ram': 0} official_image1 = images.Image(images.ImageManager(None), image_dict) image_dict = {'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822', 'name': 'multi_prop_image', 'status': "active", 'size': 20 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': TEST.tenant.id, 'container_format': 'novaImage', 'properties': {'description': u'a multi prop image', 'foo': u'foo val', 'bar': u'bar val'}, 'is_public': True, 'protected': False} multi_prop_image = images.Image(images.ImageManager(None), image_dict) # An image without name being returned based on current api image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849', 'status': "active", 'size': 8 * 1024 ** 3, 'virtual_size': None, 'min_disk': 0, 'owner': 'someothertenant', 'container_format': 'aki', 'is_public': False, 'protected': False} no_name_image = images.Image(images.ImageManager(None), image_dict) TEST.images_api.add(public_image, private_image, protected_image, public_image2, private_image2, private_image3, shared_image1, official_image1, multi_prop_image) TEST.images.add(api.glance.Image(public_image), api.glance.Image(private_image), api.glance.Image(protected_image), api.glance.Image(public_image2), api.glance.Image(private_image2), api.glance.Image(private_image3), api.glance.Image(shared_image1), api.glance.Image(official_image1), api.glance.Image(multi_prop_image)) TEST.empty_name_image = api.glance.Image(no_name_image) image_v2_dicts = [{ 'checksum': 'eb9139e4942121f22bbc2afc0400b2a4', 'container_format': 'novaImage', 'created_at': '2014-02-14T20:56:53', 'direct_url': 'swift+config://ref1/glance/' 'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5', 'disk_format': u'qcow2', 'file': '/v2/images/' 'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5/file', 'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822', 'kernel_id': 'f6ebd5f0-b110-4406-8c1e-67b28d4e85e7', 'locations': [ {'metadata': {}, 'url': 'swift+config://ref1/glance/' 'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5'}], 'min_ram': 0, 'name': 'public_image', 'image_type': u'image', 'min_disk': 0, 'owner': TEST.tenant.id, 'protected': False, 'ramdisk_id': '868efefc-4f2d-4ed8-82b1-7e35576a7a47', 'size': 20 * 1024 ** 3, 'status': 'active', 'tags': ['active_image'], 'updated_at': '2015-08-31T19:37:45Z', 'virtual_size': None, 'visibility': 'public' }, { 'checksum': None, 'container_format': 'novaImage', 'created_at': '2014-03-16T06:22:14', 'disk_format': None, 'image_type': u'image', 'file': '/v2/images/885d1cb0-9f5c-4677-9d03-175be7f9f984/file', 'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c', 'locations': [], 'min_disk': 30, 'min_ram': 0, 'name': 'protected_images', 'owner': TEST.tenant.id, 'protected': True, 'size': 2 * 1024 ** 3, 'status': "active", 'tags': ['empty_image'], 'updated_at': '2015-09-01T22:37:32Z', 'virtual_size': None, 'visibility': 'public' }, { 'checksum': 'e533283e6aac072533d1d091a7d2e413', 'container_format': 'novaImage', 'created_at': '2015-09-02T00:31:16Z', 'disk_format': 'qcow2', 'file': '/v2/images/10ca6b6b-48f4-43ac-8159-aa9e9353f5e4/file', 'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822', 'image_type': 'an image type', 'min_disk': 0, 'min_ram': 0, 'name': 'multi_prop_image', 'owner': TEST.tenant.id, 'protected': False, 'size': 20 * 1024 ** 3, 'status': 'active', 'tags': ['custom_property_image'], 'updated_at': '2015-09-02T00:31:17Z', 'virtual_size': None, 'visibility': 'public', 'description': u'a multi prop image', 'foo': u'foo val', 'bar': u'bar val' }] for fixture in image_v2_dicts: apiresource = APIResourceV2(fixture) TEST.imagesV2.add(api.glance.Image(apiresource)) metadef_dict = { 'namespace': 'namespace_1', 'display_name': 'Namespace 1', 'description': 'Mock desc 1', 'resource_type_associations': [ { 'created_at': '2014-08-21T08:39:43Z', 'prefix': 'mock', 'name': 'mock name' } ], 'visibility': 'public', 'protected': True, 'created_at': '2014-08-21T08:39:43Z', 'properties': { 'cpu_mock:mock': { 'default': '1', 'type': 'integer', 'description': 'Number of mocks.', 'title': 'mocks' } } } metadef = Namespace(metadef_dict) TEST.metadata_defs.add(metadef) metadef_dict = { 'namespace': 'namespace_2', 'display_name': 'Namespace 2', 'description': 'Mock desc 2', 'resource_type_associations': [ { 'created_at': '2014-08-21T08:39:43Z', 'prefix': 'mock', 'name': 'mock name' } ], 'visibility': 'private', 'protected': False, 'created_at': '2014-08-21T08:39:43Z', 'properties': { 'hdd_mock:mock': { 'default': '2', 'type': 'integer', 'description': 'Number of mocks.', 'title': 'mocks' } } } metadef = Namespace(metadef_dict) TEST.metadata_defs.add(metadef) metadef_dict = { 'namespace': 'namespace_3', 'display_name': 'Namespace 3', 'description': 'Mock desc 3', 'resource_type_associations': [ { 'created_at': '2014-08-21T08:39:43Z', 'prefix': 'mock', 'name': 'mock name' } ], 'visibility': 'public', 'protected': False, 'created_at': '2014-08-21T08:39:43Z', 'properties': { 'gpu_mock:mock': { 'default': '2', 'type': 'integer', 'description': 'Number of mocks.', 'title': 'mocks' } } } metadef = Namespace(metadef_dict) TEST.metadata_defs.add(metadef) metadef_dict = { 'namespace': 'namespace_4', 'display_name': 'Namespace 4', 'description': 'Mock desc 4', 'resource_type_associations': [ { 'created_at': '2014-08-21T08:39:43Z', 'prefix': 'mock', 'name': 'OS::Cinder::Volume', 'properties_target': 'user' } ], 'visibility': 'public', 'protected': True, 'created_at': '2014-08-21T08:39:43Z', 'properties': { 'ram_mock:mock': { 'default': '2', 'type': 'integer', 'description': 'Number of mocks.', 'title': 'mocks' } } } metadef = Namespace(metadef_dict) TEST.metadata_defs.add(metadef) heat-dashboard-3.0.1/heat_dashboard/test/test_data/keystone_data.py0000664000175000017500000003150713737340771025510 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy from datetime import timedelta from django.conf import settings from django.utils import datetime_safe from keystoneclient import access from keystoneclient.v2_0 import tenants from keystoneclient.v2_0 import users from keystoneclient.v3.contrib.federation import identity_providers from keystoneclient.v3.contrib.federation import mappings from keystoneclient.v3.contrib.federation import protocols from keystoneclient.v3 import domains from openstack_auth import user as auth_user from heat_dashboard.test.test_data import utils # Dummy service catalog with all service. # All endpoint URLs should point to example.com. # Try to keep them as accurate to real data as possible (ports, URIs, etc.) SERVICE_CATALOG = [ {"type": "compute", "name": "nova", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.nova.example.com:8774/v2", "internalURL": "http://int.nova.example.com:8774/v2", "publicURL": "http://public.nova.example.com:8774/v2"}, {"region": "RegionTwo", "adminURL": "http://admin.nova2.example.com:8774/v2", "internalURL": "http://int.nova2.example.com:8774/v2", "publicURL": "http://public.nova2.example.com:8774/v2"}]}, {"type": "volumev2", "name": "cinderv2", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.nova.example.com:8776/v2", "internalURL": "http://int.nova.example.com:8776/v2", "publicURL": "http://public.nova.example.com:8776/v2"}, {"region": "RegionTwo", "adminURL": "http://admin.nova.example.com:8776/v2", "internalURL": "http://int.nova.example.com:8776/v2", "publicURL": "http://public.nova.example.com:8776/v2"}]}, {"type": "image", "name": "glance", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.glance.example.com:9292", "internalURL": "http://int.glance.example.com:9292", "publicURL": "http://public.glance.example.com:9292"}]}, {"type": "identity", "name": "keystone", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.keystone.example.com:35357/v2.0", "internalURL": "http://int.keystone.example.com:5000/v2.0", "publicURL": "http://public.keystone.example.com:5000/v2.0"}]}, {"type": "object-store", "name": "swift", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.swift.example.com:8080/", "internalURL": "http://int.swift.example.com:8080/", "publicURL": "http://public.swift.example.com:8080/"}]}, {"type": "network", "name": "neutron", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.neutron.example.com:9696/", "internalURL": "http://int.neutron.example.com:9696/", "publicURL": "http://public.neutron.example.com:9696/"}]}, {"type": "ec2", "name": "EC2 Service", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.nova.example.com:8773/services/Admin", "publicURL": "http://public.nova.example.com:8773/services/Cloud", "internalURL": "http://int.nova.example.com:8773/services/Cloud"}]}, {"type": "orchestration", "name": "Heat", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", "adminURL": "http://admin.heat.example.com:8004/v1", "publicURL": "http://public.heat.example.com:8004/v1", "internalURL": "http://int.heat.example.com:8004/v1"}]} ] def data(TEST): # Make a deep copy of the catalog to avoid persisting side-effects # when tests modify the catalog. TEST.service_catalog = copy.deepcopy(SERVICE_CATALOG) TEST.tokens = utils.TestDataContainer() TEST.domains = utils.TestDataContainer() TEST.users = utils.TestDataContainer() # TEST.groups = utils.TestDataContainer() TEST.tenants = utils.TestDataContainer() # TEST.role_assignments = utils.TestDataContainer() # TEST.roles = utils.TestDataContainer() # TEST.ec2 = utils.TestDataContainer() TEST.identity_providers = utils.TestDataContainer() TEST.idp_mappings = utils.TestDataContainer() TEST.idp_protocols = utils.TestDataContainer() # admin_role_dict = {'id': '1', # 'name': 'admin'} # admin_role = roles.Role(roles.RoleManager, admin_role_dict, loaded=True) member_role_dict = {'id': "2", 'name': settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE} # member_role = roles.Role(roles.RoleManager, # member_role_dict, loaded=True) # TEST.roles.add(admin_role, member_role) # TEST.roles.admin = admin_role # TEST.roles.member = member_role domain_dict = {'id': "1", 'name': 'test_domain', 'description': "a test domain.", 'enabled': True} domain_dict_2 = {'id': "2", 'name': 'disabled_domain', 'description': "a disabled test domain.", 'enabled': False} domain_dict_3 = {'id': "3", 'name': 'another_test_domain', 'description': "another test domain.", 'enabled': True} domain = domains.Domain(domains.DomainManager, domain_dict) disabled_domain = domains.Domain(domains.DomainManager, domain_dict_2) another_domain = domains.Domain(domains.DomainManager, domain_dict_3) TEST.domains.add(domain, disabled_domain, another_domain) TEST.domain = domain # Your "current" domain user_dict = {'id': "1", 'name': 'test_user', 'description': 'test_description', 'email': 'test@example.com', 'password': 'password', 'token': 'test_token', 'project_id': '1', 'enabled': True, 'domain_id': "1"} user = users.User(None, user_dict) user_dict = {'id': "2", 'name': 'user_two', 'description': 'test_description', 'email': 'two@example.com', 'password': 'password', 'token': 'test_token', 'project_id': '1', 'enabled': True, 'domain_id': "1"} user2 = users.User(None, user_dict) user_dict = {'id': "3", 'name': 'user_three', 'description': 'test_description', 'email': 'three@example.com', 'password': 'password', 'token': 'test_token', 'project_id': '1', 'enabled': True, 'domain_id': "1"} user3 = users.User(None, user_dict) user_dict = {'id': "4", 'name': 'user_four', 'description': 'test_description', 'email': 'four@example.com', 'password': 'password', 'token': 'test_token', 'project_id': '2', 'enabled': True, 'domain_id': "2"} user4 = users.User(None, user_dict) user_dict = {'id': "5", 'name': 'user_five', 'description': 'test_description', 'email': None, 'password': 'password', 'token': 'test_token', 'project_id': '2', 'enabled': True, 'domain_id': "1"} user5 = users.User(None, user_dict) TEST.users.add(user, user2, user3, user4, user5) TEST.user = user # Your "current" user TEST.user.service_catalog = copy.deepcopy(SERVICE_CATALOG) tenant_dict = {'id': "1", 'name': 'test_tenant', 'description': "a test tenant.", 'enabled': True, 'domain_id': '1', 'domain_name': 'test_domain'} tenant_dict_2 = {'id': "2", 'name': 'disabled_tenant', 'description': "a disabled test tenant.", 'enabled': False, 'domain_id': '2', 'domain_name': 'disabled_domain'} tenant_dict_3 = {'id': "3", 'name': u'\u4e91\u89c4\u5219', 'description': "an unicode-named tenant.", 'enabled': True, 'domain_id': '2', 'domain_name': 'disabled_domain'} tenant = tenants.Tenant(tenants.TenantManager, tenant_dict) disabled_tenant = tenants.Tenant(tenants.TenantManager, tenant_dict_2) tenant_unicode = tenants.Tenant(tenants.TenantManager, tenant_dict_3) TEST.tenants.add(tenant, disabled_tenant, tenant_unicode) TEST.tenant = tenant # Your "current" tenant tomorrow = datetime_safe.datetime.now() + timedelta(days=1) expiration = tomorrow.isoformat() scoped_token_dict = { 'access': { 'token': { 'id': "test_token_id", 'expires': expiration, 'tenant': tenant_dict, 'tenants': [tenant_dict]}, 'user': { 'id': "test_user_id", 'name': "test_user", 'roles': [member_role_dict]}, 'serviceCatalog': TEST.service_catalog } } scoped_access_info = access.AccessInfo.factory(resp=None, body=scoped_token_dict) unscoped_token_dict = { 'access': { 'token': { 'id': "test_token_id", 'expires': expiration}, 'user': { 'id': "test_user_id", 'name': "test_user", 'roles': [member_role_dict]}, 'serviceCatalog': TEST.service_catalog } } unscoped_access_info = access.AccessInfo.factory(resp=None, body=unscoped_token_dict) scoped_token = auth_user.Token(scoped_access_info) unscoped_token = auth_user.Token(unscoped_access_info) TEST.tokens.add(scoped_token, unscoped_token) TEST.token = scoped_token # your "current" token. TEST.tokens.scoped_token = scoped_token TEST.tokens.unscoped_token = unscoped_token idp_dict_1 = {'id': 'idp_1', 'description': 'identity provider 1', 'enabled': True, 'remote_ids': ['rid_1', 'rid_2']} idp_1 = identity_providers.IdentityProvider( identity_providers.IdentityProviderManager, idp_dict_1, loaded=True) idp_dict_2 = {'id': 'idp_2', 'description': 'identity provider 2', 'enabled': True, 'remote_ids': ['rid_3', 'rid_4']} idp_2 = identity_providers.IdentityProvider( identity_providers.IdentityProviderManager, idp_dict_2, loaded=True) TEST.identity_providers.add(idp_1, idp_2) idp_mapping_dict = { "id": "mapping_1", "rules": [ { "local": [ { "user": { "name": "{0}" } }, { "group": { "id": "0cd5e9" } } ], "remote": [ { "type": "UserName" }, { "type": "orgPersonType", "not_any_of": [ "Contractor", "Guest" ] } ] } ] } idp_mapping = mappings.Mapping( mappings.MappingManager(None), idp_mapping_dict) TEST.idp_mappings.add(idp_mapping) idp_protocol_dict_1 = {'id': 'protocol_1', 'mapping_id': 'mapping_1'} idp_protocol = protocols.Protocol( protocols.ProtocolManager, idp_protocol_dict_1, loaded=True) TEST.idp_protocols.add(idp_protocol) heat-dashboard-3.0.1/heat_dashboard/test/test_data/nova_data.py0000664000175000017500000001654113737340771024613 0ustar zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from novaclient.v2 import availability_zones from novaclient.v2 import flavors from novaclient.v2 import keypairs from novaclient.v2 import servers from heat_dashboard.test.test_data import utils class FlavorExtraSpecs(dict): def __repr__(self): return "" % self._info def __init__(self, info): super(FlavorExtraSpecs, self).__init__() self.__dict__.update(info) self.update(info) self._info = info SERVER_DATA = """ { "server": { "OS-EXT-SRV-ATTR:instance_name": "instance-00000005", "OS-EXT-SRV-ATTR:host": "instance-host", "OS-EXT-STS:task_state": null, "addresses": { "private": [ { "version": 4, "addr": "10.0.0.1" } ] }, "links": [ { "href": "%(host)s/v1.1/%(tenant_id)s/servers/%(server_id)s", "rel": "self" }, { "href": "%(host)s/%(tenant_id)s/servers/%(server_id)s", "rel": "bookmark" } ], "image": { "id": "%(image_id)s", "links": [ { "href": "%(host)s/%(tenant_id)s/images/%(image_id)s", "rel": "bookmark" } ] }, "OS-EXT-STS:vm_state": "active", "flavor": { "id": "%(flavor_id)s", "links": [ { "href": "%(host)s/%(tenant_id)s/flavors/%(flavor_id)s", "rel": "bookmark" } ] }, "id": "%(server_id)s", "user_id": "%(user_id)s", "OS-DCF:diskConfig": "MANUAL", "accessIPv4": "", "accessIPv6": "", "progress": null, "OS-EXT-STS:power_state": 1, "config_drive": "", "status": "%(status)s", "updated": "2012-02-28T19:51:27Z", "hostId": "c461ea283faa0ab5d777073c93b126c68139e4e45934d4fc37e403c2", "key_name": "%(key_name)s", "name": "%(name)s", "created": "2012-02-28T19:51:17Z", "tenant_id": "%(tenant_id)s", "metadata": {"someMetaLabel": "someMetaData", "somehtmllabel": " optional parameter. If true, the prototype values will also be extended. * (ie. the options objects that inherit from others will also get the inherited options) * @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted * @returns {Object} */ exports.deepExtend = function (a, b, protoExtend, allowDeletion) { for (var prop in b) { if (b.hasOwnProperty(prop) || protoExtend === true) { if (b[prop] && b[prop].constructor === Object) { if (a[prop] === undefined) { a[prop] = {}; } if (a[prop].constructor === Object) { exports.deepExtend(a[prop], b[prop], protoExtend); } else { if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { delete a[prop]; } else { a[prop] = b[prop]; } } } else if (Array.isArray(b[prop])) { a[prop] = []; for (var i = 0; i < b[prop].length; i++) { a[prop].push(b[prop][i]); } } else { a[prop] = b[prop]; } } } return a; }; /** * Test whether all elements in two arrays are equal. * @param {Array} a * @param {Array} b * @return {boolean} Returns true if both arrays have the same length and same * elements. */ exports.equalArray = function (a, b) { if (a.length != b.length) return false; for (var i = 0, len = a.length; i < len; i++) { if (a[i] != b[i]) return false; } return true; }; /** * Convert an object to another type * @param {Boolean | Number | String | Date | Moment | Null | undefined} object * @param {String | undefined} type Name of the type. Available types: * 'Boolean', 'Number', 'String', * 'Date', 'Moment', ISODate', 'ASPDate'. * @return {*} object * @throws Error */ exports.convert = function (object, type) { var match; if (object === undefined) { return undefined; } if (object === null) { return null; } if (!type) { return object; } if (!(typeof type === 'string') && !(type instanceof String)) { throw new Error('Type must be a string'); } //noinspection FallthroughInSwitchStatementJS switch (type) { case 'boolean': case 'Boolean': return Boolean(object); case 'number': case 'Number': return Number(object.valueOf()); case 'string': case 'String': return String(object); case 'Date': if (exports.isNumber(object)) { return new Date(object); } if (object instanceof Date) { return new Date(object.valueOf()); } else if (moment.isMoment(object)) { return new Date(object.valueOf()); } if (exports.isString(object)) { match = ASPDateRegex.exec(object); if (match) { // object is an ASP date return new Date(Number(match[1])); // parse number } else { return moment(object).toDate(); // parse string } } else { throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); } case 'Moment': if (exports.isNumber(object)) { return moment(object); } if (object instanceof Date) { return moment(object.valueOf()); } else if (moment.isMoment(object)) { return moment(object); } if (exports.isString(object)) { match = ASPDateRegex.exec(object); if (match) { // object is an ASP date return moment(Number(match[1])); // parse number } else { return moment(object); // parse string } } else { throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); } case 'ISODate': if (exports.isNumber(object)) { return new Date(object); } else if (object instanceof Date) { return object.toISOString(); } else if (moment.isMoment(object)) { return object.toDate().toISOString(); } else if (exports.isString(object)) { match = ASPDateRegex.exec(object); if (match) { // object is an ASP date return new Date(Number(match[1])).toISOString(); // parse number } else { return new Date(object).toISOString(); // parse string } } else { throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); } case 'ASPDate': if (exports.isNumber(object)) { return '/Date(' + object + ')/'; } else if (object instanceof Date) { return '/Date(' + object.valueOf() + ')/'; } else if (exports.isString(object)) { match = ASPDateRegex.exec(object); var value; if (match) { // object is an ASP date value = new Date(Number(match[1])).valueOf(); // parse number } else { value = new Date(object).valueOf(); // parse string } return '/Date(' + value + ')/'; } else { throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); } default: throw new Error('Unknown type "' + type + '"'); } }; // parse ASP.Net Date pattern, // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' // code from http://momentjs.com/ var ASPDateRegex = /^\/?Date\((\-?\d+)/i; /** * Get the type of an object, for example exports.getType([]) returns 'Array' * @param {*} object * @return {String} type */ exports.getType = function (object) { var type = typeof object; if (type == 'object') { if (object === null) { return 'null'; } if (object instanceof Boolean) { return 'Boolean'; } if (object instanceof Number) { return 'Number'; } if (object instanceof String) { return 'String'; } if (Array.isArray(object)) { return 'Array'; } if (object instanceof Date) { return 'Date'; } return 'Object'; } else if (type == 'number') { return 'Number'; } else if (type == 'boolean') { return 'Boolean'; } else if (type == 'string') { return 'String'; } else if (type === undefined) { return 'undefined'; } return type; }; /** * Used to extend an array and copy it. This is used to propagate paths recursively. * * @param arr * @param newValue * @returns {Array} */ exports.copyAndExtendArray = function (arr, newValue) { var newArr = []; for (var i = 0; i < arr.length; i++) { newArr.push(arr[i]); } newArr.push(newValue); return newArr; }; /** * Used to extend an array and copy it. This is used to propagate paths recursively. * * @param arr * @param newValue * @returns {Array} */ exports.copyArray = function (arr) { var newArr = []; for (var i = 0; i < arr.length; i++) { newArr.push(arr[i]); } return newArr; }; /** * Retrieve the absolute left value of a DOM element * @param {Element} elem A dom element, for example a div * @return {number} left The absolute left position of this element * in the browser page. */ exports.getAbsoluteLeft = function (elem) { return elem.getBoundingClientRect().left; }; /** * Retrieve the absolute top value of a DOM element * @param {Element} elem A dom element, for example a div * @return {number} top The absolute top position of this element * in the browser page. */ exports.getAbsoluteTop = function (elem) { return elem.getBoundingClientRect().top; }; /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ exports.addClassName = function (elem, className) { var classes = elem.className.split(' '); if (classes.indexOf(className) == -1) { classes.push(className); // add the class to the array elem.className = classes.join(' '); } }; /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ exports.removeClassName = function (elem, className) { var classes = elem.className.split(' '); var index = classes.indexOf(className); if (index != -1) { classes.splice(index, 1); // remove the class from the array elem.className = classes.join(' '); } }; /** * For each method for both arrays and objects. * In case of an array, the built-in Array.forEach() is applied. * In case of an Object, the method loops over all properties of the object. * @param {Object | Array} object An Object or Array * @param {function} callback Callback method, called for each item in * the object or array with three parameters: * callback(value, index, object) */ exports.forEach = function (object, callback) { var i, len; if (Array.isArray(object)) { // array for (i = 0, len = object.length; i < len; i++) { callback(object[i], i, object); } } else { // object for (i in object) { if (object.hasOwnProperty(i)) { callback(object[i], i, object); } } } }; /** * Convert an object into an array: all objects properties are put into the * array. The resulting array is unordered. * @param {Object} object * @param {Array} array */ exports.toArray = function (object) { var array = []; for (var prop in object) { if (object.hasOwnProperty(prop)) array.push(object[prop]); } return array; }; /** * Update a property in an object * @param {Object} object * @param {String} key * @param {*} value * @return {Boolean} changed */ exports.updateProperty = function (object, key, value) { if (object[key] !== value) { object[key] = value; return true; } else { return false; } }; /** * Add and event listener. Works for all browsers * @param {Element} element An html element * @param {string} action The action, for example "click", * without the prefix "on" * @param {function} listener The callback function to be executed * @param {boolean} [useCapture] */ exports.addEventListener = function (element, action, listener, useCapture) { if (element.addEventListener) { if (useCapture === undefined) useCapture = false; if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { action = 'DOMMouseScroll'; // For Firefox } element.addEventListener(action, listener, useCapture); } else { element.attachEvent('on' + action, listener); // IE browsers } }; /** * Remove an event listener from an element * @param {Element} element An html dom element * @param {string} action The name of the event, for example "mousedown" * @param {function} listener The listener function * @param {boolean} [useCapture] */ exports.removeEventListener = function (element, action, listener, useCapture) { if (element.removeEventListener) { // non-IE browsers if (useCapture === undefined) useCapture = false; if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { action = 'DOMMouseScroll'; // For Firefox } element.removeEventListener(action, listener, useCapture); } else { // IE browsers element.detachEvent('on' + action, listener); } }; /** * Cancels the event if it is cancelable, without stopping further propagation of the event. */ exports.preventDefault = function (event) { if (!event) event = window.event; if (event.preventDefault) { event.preventDefault(); // non-IE browsers } else { event.returnValue = false; // IE browsers } }; /** * Get HTML element which is the target of the event * @param {Event} event * @return {Element} target element */ exports.getTarget = function (event) { // code from http://www.quirksmode.org/js/events_properties.html if (!event) { event = window.event; } var target; if (event.target) { target = event.target; } else if (event.srcElement) { target = event.srcElement; } if (target.nodeType != undefined && target.nodeType == 3) { // defeat Safari bug target = target.parentNode; } return target; }; /** * Check if given element contains given parent somewhere in the DOM tree * @param {Element} element * @param {Element} parent */ exports.hasParent = function (element, parent) { var e = element; while (e) { if (e === parent) { return true; } e = e.parentNode; } return false; }; exports.option = {}; /** * Convert a value into a boolean * @param {Boolean | function | undefined} value * @param {Boolean} [defaultValue] * @returns {Boolean} bool */ exports.option.asBoolean = function (value, defaultValue) { if (typeof value == 'function') { value = value(); } if (value != null) { return value != false; } return defaultValue || null; }; /** * Convert a value into a number * @param {Boolean | function | undefined} value * @param {Number} [defaultValue] * @returns {Number} number */ exports.option.asNumber = function (value, defaultValue) { if (typeof value == 'function') { value = value(); } if (value != null) { return Number(value) || defaultValue || null; } return defaultValue || null; }; /** * Convert a value into a string * @param {String | function | undefined} value * @param {String} [defaultValue] * @returns {String} str */ exports.option.asString = function (value, defaultValue) { if (typeof value == 'function') { value = value(); } if (value != null) { return String(value); } return defaultValue || null; }; /** * Convert a size or location into a string with pixels or a percentage * @param {String | Number | function | undefined} value * @param {String} [defaultValue] * @returns {String} size */ exports.option.asSize = function (value, defaultValue) { if (typeof value == 'function') { value = value(); } if (exports.isString(value)) { return value; } else if (exports.isNumber(value)) { return value + 'px'; } else { return defaultValue || null; } }; /** * Convert a value into a DOM element * @param {HTMLElement | function | undefined} value * @param {HTMLElement} [defaultValue] * @returns {HTMLElement | null} dom */ exports.option.asElement = function (value, defaultValue) { if (typeof value == 'function') { value = value(); } return value || defaultValue || null; }; /** * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb * * @param {String} hex * @returns {{r: *, g: *, b: *}} | 255 range */ exports.hexToRGB = function (hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function (m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }; /** * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. * @param color * @param opacity * @returns {*} */ exports.overrideOpacity = function (color, opacity) { if (color.indexOf('rgba') != -1) { return color; } else if (color.indexOf('rgb') != -1) { var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(','); return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')'; } else { var rgb = exports.hexToRGB(color); if (rgb == null) { return color; } else { return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')'; } } }; /** * * @param red 0 -- 255 * @param green 0 -- 255 * @param blue 0 -- 255 * @returns {string} * @constructor */ exports.RGBToHex = function (red, green, blue) { return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); }; /** * Parse a color property into an object with border, background, and * highlight colors * @param {Object | String} color * @return {Object} colorObject */ exports.parseColor = function (color) { var c; if (exports.isString(color) === true) { if (exports.isValidRGB(color) === true) { var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { return parseInt(value); }); color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]); } if (exports.isValidHex(color) === true) { var hsv = exports.hexToHSV(color); var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) }; var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 }; var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v); var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v); c = { background: color, border: darkerColorHex, highlight: { background: lighterColorHex, border: darkerColorHex }, hover: { background: lighterColorHex, border: darkerColorHex } }; } else { c = { background: color, border: color, highlight: { background: color, border: color }, hover: { background: color, border: color } }; } } else { c = {}; c.background = color.background || undefined; c.border = color.border || undefined; if (exports.isString(color.highlight)) { c.highlight = { border: color.highlight, background: color.highlight }; } else { c.highlight = {}; c.highlight.background = color.highlight && color.highlight.background || undefined; c.highlight.border = color.highlight && color.highlight.border || undefined; } if (exports.isString(color.hover)) { c.hover = { border: color.hover, background: color.hover }; } else { c.hover = {}; c.hover.background = color.hover && color.hover.background || undefined; c.hover.border = color.hover && color.hover.border || undefined; } } return c; }; /** * http://www.javascripter.net/faq/rgb2hsv.htm * * @param red * @param green * @param blue * @returns {*} * @constructor */ exports.RGBToHSV = function (red, green, blue) { red = red / 255;green = green / 255;blue = blue / 255; var minRGB = Math.min(red, Math.min(green, blue)); var maxRGB = Math.max(red, Math.max(green, blue)); // Black-gray-white if (minRGB == maxRGB) { return { h: 0, s: 0, v: minRGB }; } // Colors other than black-gray-white: var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red; var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5; var hue = 60 * (h - d / (maxRGB - minRGB)) / 360; var saturation = (maxRGB - minRGB) / maxRGB; var value = maxRGB; return { h: hue, s: saturation, v: value }; }; var cssUtil = { // split a string with css styles into an object with key/values split: function split(cssText) { var styles = {}; cssText.split(';').forEach(function (style) { if (style.trim() != '') { var parts = style.split(':'); var key = parts[0].trim(); var value = parts[1].trim(); styles[key] = value; } }); return styles; }, // build a css text string from an object with key/values join: function join(styles) { return Object.keys(styles).map(function (key) { return key + ': ' + styles[key]; }).join('; '); } }; /** * Append a string with css styles to an element * @param {Element} element * @param {String} cssText */ exports.addCssText = function (element, cssText) { var currentStyles = cssUtil.split(element.style.cssText); var newStyles = cssUtil.split(cssText); var styles = exports.extend(currentStyles, newStyles); element.style.cssText = cssUtil.join(styles); }; /** * Remove a string with css styles from an element * @param {Element} element * @param {String} cssText */ exports.removeCssText = function (element, cssText) { var styles = cssUtil.split(element.style.cssText); var removeStyles = cssUtil.split(cssText); for (var key in removeStyles) { if (removeStyles.hasOwnProperty(key)) { delete styles[key]; } } element.style.cssText = cssUtil.join(styles); }; /** * https://gist.github.com/mjijackson/5311256 * @param h * @param s * @param v * @returns {{r: number, g: number, b: number}} * @constructor */ exports.HSVToRGB = function (h, s, v) { var r, g, b; var i = Math.floor(h * 6); var f = h * 6 - i; var p = v * (1 - s); var q = v * (1 - f * s); var t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: r = v, g = t, b = p;break; case 1: r = q, g = v, b = p;break; case 2: r = p, g = v, b = t;break; case 3: r = p, g = q, b = v;break; case 4: r = t, g = p, b = v;break; case 5: r = v, g = p, b = q;break; } return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; }; exports.HSVToHex = function (h, s, v) { var rgb = exports.HSVToRGB(h, s, v); return exports.RGBToHex(rgb.r, rgb.g, rgb.b); }; exports.hexToHSV = function (hex) { var rgb = exports.hexToRGB(hex); return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); }; exports.isValidHex = function (hex) { var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); return isOk; }; exports.isValidRGB = function (rgb) { rgb = rgb.replace(' ', ''); var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb); return isOk; }; exports.isValidRGBA = function (rgba) { rgba = rgba.replace(' ', ''); var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba); return isOk; }; /** * This recursively redirects the prototype of JSON objects to the referenceObject * This is used for default options. * * @param referenceObject * @returns {*} */ exports.selectiveBridgeObject = function (fields, referenceObject) { if (typeof referenceObject == 'object') { var objectTo = Object.create(referenceObject); for (var i = 0; i < fields.length; i++) { if (referenceObject.hasOwnProperty(fields[i])) { if (typeof referenceObject[fields[i]] == 'object') { objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]); } } } return objectTo; } else { return null; } }; /** * This recursively redirects the prototype of JSON objects to the referenceObject * This is used for default options. * * @param referenceObject * @returns {*} */ exports.bridgeObject = function (referenceObject) { if (typeof referenceObject == 'object') { var objectTo = Object.create(referenceObject); for (var i in referenceObject) { if (referenceObject.hasOwnProperty(i)) { if (typeof referenceObject[i] == 'object') { objectTo[i] = exports.bridgeObject(referenceObject[i]); } } } return objectTo; } else { return null; } }; /** * this is used to set the options of subobjects in the options object. A requirement of these subobjects * is that they have an 'enabled' element which is optional for the user but mandatory for the program. * * @param [object] mergeTarget | this is either this.options or the options used for the groups. * @param [object] options | options * @param [String] option | this is the option key in the options argument * @private */ exports.mergeOptions = function (mergeTarget, options, option) { var allowDeletion = arguments[3] === undefined ? false : arguments[3]; if (options[option] === null) { mergeTarget[option] = undefined; delete mergeTarget[option]; } else { if (options[option] !== undefined) { if (typeof options[option] === 'boolean') { mergeTarget[option].enabled = options[option]; } else { if (options[option].enabled === undefined) { mergeTarget[option].enabled = true; } for (var prop in options[option]) { if (options[option].hasOwnProperty(prop)) { mergeTarget[option][prop] = options[option][prop]; } } } } } }; /** * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses * this function will then iterate in both directions over this sorted list to find all visible items. * * @param {Item[]} orderedItems | Items ordered by start * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher * @param {String} field * @param {String} field2 * @returns {number} * @private */ exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { var maxIterations = 10000; var iteration = 0; var low = 0; var high = orderedItems.length - 1; while (low <= high && iteration < maxIterations) { var middle = Math.floor((low + high) / 2); var item = orderedItems[middle]; var value = field2 === undefined ? item[field] : item[field][field2]; var searchResult = searchFunction(value); if (searchResult == 0) { // jihaa, found a visible item! return middle; } else if (searchResult == -1) { // it is too small --> increase low low = middle + 1; } else { // it is too big --> decrease high high = middle - 1; } iteration++; } return -1; }; /** * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of * two values, we return either the one before or the one after, depending on user input * If it is found, we return the index, else -1. * * @param {Array} orderedItems * @param {{start: number, end: number}} target * @param {String} field * @param {String} sidePreference 'before' or 'after' * @returns {number} * @private */ exports.binarySearchValue = function (orderedItems, target, field, sidePreference) { var maxIterations = 10000; var iteration = 0; var low = 0; var high = orderedItems.length - 1; var prevValue, value, nextValue, middle; while (low <= high && iteration < maxIterations) { // get a new guess middle = Math.floor(0.5 * (high + low)); prevValue = orderedItems[Math.max(0, middle - 1)][field]; value = orderedItems[middle][field]; nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; if (value == target) { // we found the target return middle; } else if (prevValue < target && value > target) { // target is in between of the previous and the current return sidePreference == 'before' ? Math.max(0, middle - 1) : middle; } else if (value < target && nextValue > target) { // target is in between of the current and the next return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1); } else { // didnt find the target, we need to change our boundaries. if (value < target) { // it is too small --> increase low low = middle + 1; } else { // it is too big --> decrease high high = middle - 1; } } iteration++; } // didnt find anything. Return -1. return -1; }; /* * Easing Functions - inspired from http://gizma.com/easing/ * only considering the t value for the range [0, 1] => [0, 1] * https://gist.github.com/gre/1650294 */ exports.easingFunctions = { // no easing, no acceleration linear: function linear(t) { return t; }, // accelerating from zero velocity easeInQuad: function easeInQuad(t) { return t * t; }, // decelerating to zero velocity easeOutQuad: function easeOutQuad(t) { return t * (2 - t); }, // acceleration until halfway, then deceleration easeInOutQuad: function easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }, // accelerating from zero velocity easeInCubic: function easeInCubic(t) { return t * t * t; }, // decelerating to zero velocity easeOutCubic: function easeOutCubic(t) { return --t * t * t + 1; }, // acceleration until halfway, then deceleration easeInOutCubic: function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; }, // accelerating from zero velocity easeInQuart: function easeInQuart(t) { return t * t * t * t; }, // decelerating to zero velocity easeOutQuart: function easeOutQuart(t) { return 1 - --t * t * t * t; }, // acceleration until halfway, then deceleration easeInOutQuart: function easeInOutQuart(t) { return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; }, // accelerating from zero velocity easeInQuint: function easeInQuint(t) { return t * t * t * t * t; }, // decelerating to zero velocity easeOutQuint: function easeOutQuint(t) { return 1 + --t * t * t * t * t; }, // acceleration until halfway, then deceleration easeInOutQuint: function easeInOutQuint(t) { return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; } }; /***/ }, /* 2 */ /***/ function(module, exports, __webpack_require__) { // first check if moment.js is already loaded in the browser window, if so, // use this instance. Else, load via commonjs. 'use strict'; module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(3); /***/ }, /* 3 */ /***/ function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(module) {//! moment.js //! version : 2.10.3 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors //! license : MIT //! momentjs.com (function (global, factory) { true ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : global.moment = factory() }(this, function () { 'use strict'; var hookCallback; function utils_hooks__hooks () { return hookCallback.apply(null, arguments); } // This is done to register the method called with moment() // without creating circular dependencies. function setHookCallback (callback) { hookCallback = callback; } function isArray(input) { return Object.prototype.toString.call(input) === '[object Array]'; } function isDate(input) { return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; } function map(arr, fn) { var res = [], i; for (i = 0; i < arr.length; ++i) { res.push(fn(arr[i], i)); } return res; } function hasOwnProp(a, b) { return Object.prototype.hasOwnProperty.call(a, b); } function extend(a, b) { for (var i in b) { if (hasOwnProp(b, i)) { a[i] = b[i]; } } if (hasOwnProp(b, 'toString')) { a.toString = b.toString; } if (hasOwnProp(b, 'valueOf')) { a.valueOf = b.valueOf; } return a; } function create_utc__createUTC (input, format, locale, strict) { return createLocalOrUTC(input, format, locale, strict, true).utc(); } function defaultParsingFlags() { // We need to deep clone this object. return { empty : false, unusedTokens : [], unusedInput : [], overflow : -2, charsLeftOver : 0, nullInput : false, invalidMonth : null, invalidFormat : false, userInvalidated : false, iso : false }; } function getParsingFlags(m) { if (m._pf == null) { m._pf = defaultParsingFlags(); } return m._pf; } function valid__isValid(m) { if (m._isValid == null) { var flags = getParsingFlags(m); m._isValid = !isNaN(m._d.getTime()) && flags.overflow < 0 && !flags.empty && !flags.invalidMonth && !flags.nullInput && !flags.invalidFormat && !flags.userInvalidated; if (m._strict) { m._isValid = m._isValid && flags.charsLeftOver === 0 && flags.unusedTokens.length === 0 && flags.bigHour === undefined; } } return m._isValid; } function valid__createInvalid (flags) { var m = create_utc__createUTC(NaN); if (flags != null) { extend(getParsingFlags(m), flags); } else { getParsingFlags(m).userInvalidated = true; } return m; } var momentProperties = utils_hooks__hooks.momentProperties = []; function copyConfig(to, from) { var i, prop, val; if (typeof from._isAMomentObject !== 'undefined') { to._isAMomentObject = from._isAMomentObject; } if (typeof from._i !== 'undefined') { to._i = from._i; } if (typeof from._f !== 'undefined') { to._f = from._f; } if (typeof from._l !== 'undefined') { to._l = from._l; } if (typeof from._strict !== 'undefined') { to._strict = from._strict; } if (typeof from._tzm !== 'undefined') { to._tzm = from._tzm; } if (typeof from._isUTC !== 'undefined') { to._isUTC = from._isUTC; } if (typeof from._offset !== 'undefined') { to._offset = from._offset; } if (typeof from._pf !== 'undefined') { to._pf = getParsingFlags(from); } if (typeof from._locale !== 'undefined') { to._locale = from._locale; } if (momentProperties.length > 0) { for (i in momentProperties) { prop = momentProperties[i]; val = from[prop]; if (typeof val !== 'undefined') { to[prop] = val; } } } return to; } var updateInProgress = false; // Moment prototype object function Moment(config) { copyConfig(this, config); this._d = new Date(+config._d); // Prevent infinite loop in case updateOffset creates new moment // objects. if (updateInProgress === false) { updateInProgress = true; utils_hooks__hooks.updateOffset(this); updateInProgress = false; } } function isMoment (obj) { return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); } function toInt(argumentForCoercion) { var coercedNumber = +argumentForCoercion, value = 0; if (coercedNumber !== 0 && isFinite(coercedNumber)) { if (coercedNumber >= 0) { value = Math.floor(coercedNumber); } else { value = Math.ceil(coercedNumber); } } return value; } function compareArrays(array1, array2, dontConvert) { var len = Math.min(array1.length, array2.length), lengthDiff = Math.abs(array1.length - array2.length), diffs = 0, i; for (i = 0; i < len; i++) { if ((dontConvert && array1[i] !== array2[i]) || (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { diffs++; } } return diffs + lengthDiff; } function Locale() { } var locales = {}; var globalLocale; function normalizeLocale(key) { return key ? key.toLowerCase().replace('_', '-') : key; } // pick the locale from the array // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root function chooseLocale(names) { var i = 0, j, next, locale, split; while (i < names.length) { split = normalizeLocale(names[i]).split('-'); j = split.length; next = normalizeLocale(names[i + 1]); next = next ? next.split('-') : null; while (j > 0) { locale = loadLocale(split.slice(0, j).join('-')); if (locale) { return locale; } if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { //the next array item is better than a shallower substring of this one break; } j--; } i++; } return null; } function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && typeof module !== 'undefined' && module && module.exports) { try { oldLocale = globalLocale._abbr; !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); // because defineLocale currently also sets the global locale, we // want to undo that for lazy loaded locales locale_locales__getSetGlobalLocale(oldLocale); } catch (e) { } } return locales[name]; } // This function will load locale and then set the global locale. If // no arguments are passed in, it will simply return the current global // locale key. function locale_locales__getSetGlobalLocale (key, values) { var data; if (key) { if (typeof values === 'undefined') { data = locale_locales__getLocale(key); } else { data = defineLocale(key, values); } if (data) { // moment.duration._locale = moment._locale = data; globalLocale = data; } } return globalLocale._abbr; } function defineLocale (name, values) { if (values !== null) { values.abbr = name; if (!locales[name]) { locales[name] = new Locale(); } locales[name].set(values); // backwards compat for now: also set the locale locale_locales__getSetGlobalLocale(name); return locales[name]; } else { // useful for testing delete locales[name]; return null; } } // returns locale data function locale_locales__getLocale (key) { var locale; if (key && key._locale && key._locale._abbr) { key = key._locale._abbr; } if (!key) { return globalLocale; } if (!isArray(key)) { //short-circuit everything else locale = loadLocale(key); if (locale) { return locale; } key = [key]; } return chooseLocale(key); } var aliases = {}; function addUnitAlias (unit, shorthand) { var lowerCase = unit.toLowerCase(); aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; } function normalizeUnits(units) { return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; } function normalizeObjectUnits(inputObject) { var normalizedInput = {}, normalizedProp, prop; for (prop in inputObject) { if (hasOwnProp(inputObject, prop)) { normalizedProp = normalizeUnits(prop); if (normalizedProp) { normalizedInput[normalizedProp] = inputObject[prop]; } } } return normalizedInput; } function makeGetSet (unit, keepTime) { return function (value) { if (value != null) { get_set__set(this, unit, value); utils_hooks__hooks.updateOffset(this, keepTime); return this; } else { return get_set__get(this, unit); } }; } function get_set__get (mom, unit) { return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); } function get_set__set (mom, unit, value) { return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); } // MOMENTS function getSet (units, value) { var unit; if (typeof units === 'object') { for (unit in units) { this.set(unit, units[unit]); } } else { units = normalizeUnits(units); if (typeof this[units] === 'function') { return this[units](value); } } return this; } function zeroFill(number, targetLength, forceSign) { var output = '' + Math.abs(number), sign = number >= 0; while (output.length < targetLength) { output = '0' + output; } return (sign ? (forceSign ? '+' : '') : '-') + output; } var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; var formatFunctions = {}; var formatTokenFunctions = {}; // token: 'M' // padded: ['MM', 2] // ordinal: 'Mo' // callback: function () { this.month() + 1 } function addFormatToken (token, padded, ordinal, callback) { var func = callback; if (typeof callback === 'string') { func = function () { return this[callback](); }; } if (token) { formatTokenFunctions[token] = func; } if (padded) { formatTokenFunctions[padded[0]] = function () { return zeroFill(func.apply(this, arguments), padded[1], padded[2]); }; } if (ordinal) { formatTokenFunctions[ordinal] = function () { return this.localeData().ordinal(func.apply(this, arguments), token); }; } } function removeFormattingTokens(input) { if (input.match(/\[[\s\S]/)) { return input.replace(/^\[|\]$/g, ''); } return input.replace(/\\/g, ''); } function makeFormatFunction(format) { var array = format.match(formattingTokens), i, length; for (i = 0, length = array.length; i < length; i++) { if (formatTokenFunctions[array[i]]) { array[i] = formatTokenFunctions[array[i]]; } else { array[i] = removeFormattingTokens(array[i]); } } return function (mom) { var output = ''; for (i = 0; i < length; i++) { output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; } return output; }; } // format date using native date object function formatMoment(m, format) { if (!m.isValid()) { return m.localeData().invalidDate(); } format = expandFormat(format, m.localeData()); if (!formatFunctions[format]) { formatFunctions[format] = makeFormatFunction(format); } return formatFunctions[format](m); } function expandFormat(format, locale) { var i = 5; function replaceLongDateFormatTokens(input) { return locale.longDateFormat(input) || input; } localFormattingTokens.lastIndex = 0; while (i >= 0 && localFormattingTokens.test(format)) { format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); localFormattingTokens.lastIndex = 0; i -= 1; } return format; } var match1 = /\d/; // 0 - 9 var match2 = /\d\d/; // 00 - 99 var match3 = /\d{3}/; // 000 - 999 var match4 = /\d{4}/; // 0000 - 9999 var match6 = /[+-]?\d{6}/; // -999999 - 999999 var match1to2 = /\d\d?/; // 0 - 99 var match1to3 = /\d{1,3}/; // 0 - 999 var match1to4 = /\d{1,4}/; // 0 - 9999 var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 var matchUnsigned = /\d+/; // 0 - inf var matchSigned = /[+-]?\d+/; // -inf - inf var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 // any word (or two) characters or numbers including two/three word month in arabic. var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; var regexes = {}; function addRegexToken (token, regex, strictRegex) { regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { return (isStrict && strictRegex) ? strictRegex : regex; }; } function getParseRegexForToken (token, config) { if (!hasOwnProp(regexes, token)) { return new RegExp(unescapeFormat(token)); } return regexes[token](config._strict, config._locale); } // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript function unescapeFormat(s) { return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { return p1 || p2 || p3 || p4; }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } var tokens = {}; function addParseToken (token, callback) { var i, func = callback; if (typeof token === 'string') { token = [token]; } if (typeof callback === 'number') { func = function (input, array) { array[callback] = toInt(input); }; } for (i = 0; i < token.length; i++) { tokens[token[i]] = func; } } function addWeekParseToken (token, callback) { addParseToken(token, function (input, array, config, token) { config._w = config._w || {}; callback(input, config._w, config, token); }); } function addTimeToArrayFromToken(token, input, config) { if (input != null && hasOwnProp(tokens, token)) { tokens[token](input, config._a, config, token); } } var YEAR = 0; var MONTH = 1; var DATE = 2; var HOUR = 3; var MINUTE = 4; var SECOND = 5; var MILLISECOND = 6; function daysInMonth(year, month) { return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); } // FORMATTING addFormatToken('M', ['MM', 2], 'Mo', function () { return this.month() + 1; }); addFormatToken('MMM', 0, 0, function (format) { return this.localeData().monthsShort(this, format); }); addFormatToken('MMMM', 0, 0, function (format) { return this.localeData().months(this, format); }); // ALIASES addUnitAlias('month', 'M'); // PARSING addRegexToken('M', match1to2); addRegexToken('MM', match1to2, match2); addRegexToken('MMM', matchWord); addRegexToken('MMMM', matchWord); addParseToken(['M', 'MM'], function (input, array) { array[MONTH] = toInt(input) - 1; }); addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { var month = config._locale.monthsParse(input, token, config._strict); // if we didn't find a month name, mark the date as invalid. if (month != null) { array[MONTH] = month; } else { getParsingFlags(config).invalidMonth = input; } }); // LOCALES var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); function localeMonths (m) { return this._months[m.month()]; } var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); function localeMonthsShort (m) { return this._monthsShort[m.month()]; } function localeMonthsParse (monthName, format, strict) { var i, mom, regex; if (!this._monthsParse) { this._monthsParse = []; this._longMonthsParse = []; this._shortMonthsParse = []; } for (i = 0; i < 12; i++) { // make the regex if we don't have it already mom = create_utc__createUTC([2000, i]); if (strict && !this._longMonthsParse[i]) { this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); } if (!strict && !this._monthsParse[i]) { regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); } // test the regex if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { return i; } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { return i; } else if (!strict && this._monthsParse[i].test(monthName)) { return i; } } } // MOMENTS function setMonth (mom, value) { var dayOfMonth; // TODO: Move this out of here! if (typeof value === 'string') { value = mom.localeData().monthsParse(value); // TODO: Another silent failure? if (typeof value !== 'number') { return mom; } } dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); return mom; } function getSetMonth (value) { if (value != null) { setMonth(this, value); utils_hooks__hooks.updateOffset(this, true); return this; } else { return get_set__get(this, 'Month'); } } function getDaysInMonth () { return daysInMonth(this.year(), this.month()); } function checkOverflow (m) { var overflow; var a = m._a; if (a && getParsingFlags(m).overflow === -2) { overflow = a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : -1; if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { overflow = DATE; } getParsingFlags(m).overflow = overflow; } return m; } function warn(msg) { if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { console.warn('Deprecation warning: ' + msg); } } function deprecate(msg, fn) { var firstTime = true, msgWithStack = msg + '\n' + (new Error()).stack; return extend(function () { if (firstTime) { warn(msgWithStack); firstTime = false; } return fn.apply(this, arguments); }, fn); } var deprecations = {}; function deprecateSimple(name, msg) { if (!deprecations[name]) { warn(msg); deprecations[name] = true; } } utils_hooks__hooks.suppressDeprecationWarnings = false; var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; var isoDates = [ ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], ['GGGG-[W]WW', /\d{4}-W\d{2}/], ['YYYY-DDD', /\d{4}-\d{3}/] ]; // iso time formats and regexes var isoTimes = [ ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], ['HH:mm', /(T| )\d\d:\d\d/], ['HH', /(T| )\d\d/] ]; var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; // date from iso format function configFromISO(config) { var i, l, string = config._i, match = from_string__isoRegex.exec(string); if (match) { getParsingFlags(config).iso = true; for (i = 0, l = isoDates.length; i < l; i++) { if (isoDates[i][1].exec(string)) { // match[5] should be 'T' or undefined config._f = isoDates[i][0] + (match[6] || ' '); break; } } for (i = 0, l = isoTimes.length; i < l; i++) { if (isoTimes[i][1].exec(string)) { config._f += isoTimes[i][0]; break; } } if (string.match(matchOffset)) { config._f += 'Z'; } configFromStringAndFormat(config); } else { config._isValid = false; } } // date from iso format or fallback function configFromString(config) { var matched = aspNetJsonRegex.exec(config._i); if (matched !== null) { config._d = new Date(+matched[1]); return; } configFromISO(config); if (config._isValid === false) { delete config._isValid; utils_hooks__hooks.createFromInputFallback(config); } } utils_hooks__hooks.createFromInputFallback = deprecate( 'moment construction falls back to js Date. This is ' + 'discouraged and will be removed in upcoming major ' + 'release. Please refer to ' + 'https://github.com/moment/moment/issues/1407 for more info.', function (config) { config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); } ); function createDate (y, m, d, h, M, s, ms) { //can't just apply() to create a date: //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply var date = new Date(y, m, d, h, M, s, ms); //the date constructor doesn't accept years < 1970 if (y < 1970) { date.setFullYear(y); } return date; } function createUTCDate (y) { var date = new Date(Date.UTC.apply(null, arguments)); if (y < 1970) { date.setUTCFullYear(y); } return date; } addFormatToken(0, ['YY', 2], 0, function () { return this.year() % 100; }); addFormatToken(0, ['YYYY', 4], 0, 'year'); addFormatToken(0, ['YYYYY', 5], 0, 'year'); addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); // ALIASES addUnitAlias('year', 'y'); // PARSING addRegexToken('Y', matchSigned); addRegexToken('YY', match1to2, match2); addRegexToken('YYYY', match1to4, match4); addRegexToken('YYYYY', match1to6, match6); addRegexToken('YYYYYY', match1to6, match6); addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); addParseToken('YY', function (input, array) { array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); }); // HELPERS function daysInYear(year) { return isLeapYear(year) ? 366 : 365; } function isLeapYear(year) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } // HOOKS utils_hooks__hooks.parseTwoDigitYear = function (input) { return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); }; // MOMENTS var getSetYear = makeGetSet('FullYear', false); function getIsLeapYear () { return isLeapYear(this.year()); } addFormatToken('w', ['ww', 2], 'wo', 'week'); addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); // ALIASES addUnitAlias('week', 'w'); addUnitAlias('isoWeek', 'W'); // PARSING addRegexToken('w', match1to2); addRegexToken('ww', match1to2, match2); addRegexToken('W', match1to2); addRegexToken('WW', match1to2, match2); addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { week[token.substr(0, 1)] = toInt(input); }); // HELPERS // firstDayOfWeek 0 = sun, 6 = sat // the day of the week that starts the week // (usually sunday or monday) // firstDayOfWeekOfYear 0 = sun, 6 = sat // the first week is the week that contains the first // of this day of the week // (eg. ISO weeks use thursday (4)) function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { var end = firstDayOfWeekOfYear - firstDayOfWeek, daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), adjustedMoment; if (daysToDayOfWeek > end) { daysToDayOfWeek -= 7; } if (daysToDayOfWeek < end - 7) { daysToDayOfWeek += 7; } adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); return { week: Math.ceil(adjustedMoment.dayOfYear() / 7), year: adjustedMoment.year() }; } // LOCALES function localeWeek (mom) { return weekOfYear(mom, this._week.dow, this._week.doy).week; } var defaultLocaleWeek = { dow : 0, // Sunday is the first day of the week. doy : 6 // The week that contains Jan 1st is the first week of the year. }; function localeFirstDayOfWeek () { return this._week.dow; } function localeFirstDayOfYear () { return this._week.doy; } // MOMENTS function getSetWeek (input) { var week = this.localeData().week(this); return input == null ? week : this.add((input - week) * 7, 'd'); } function getSetISOWeek (input) { var week = weekOfYear(this, 1, 4).week; return input == null ? week : this.add((input - week) * 7, 'd'); } addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); // ALIASES addUnitAlias('dayOfYear', 'DDD'); // PARSING addRegexToken('DDD', match1to3); addRegexToken('DDDD', match3); addParseToken(['DDD', 'DDDD'], function (input, array, config) { config._dayOfYear = toInt(input); }); // HELPERS //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { var d = createUTCDate(year, 0, 1).getUTCDay(); var daysToAdd; var dayOfYear; d = d === 0 ? 7 : d; weekday = weekday != null ? weekday : firstDayOfWeek; daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; return { year : dayOfYear > 0 ? year : year - 1, dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear }; } // MOMENTS function getSetDayOfYear (input) { var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); } // Pick the first defined of two or three arguments. function defaults(a, b, c) { if (a != null) { return a; } if (b != null) { return b; } return c; } function currentDateArray(config) { var now = new Date(); if (config._useUTC) { return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; } return [now.getFullYear(), now.getMonth(), now.getDate()]; } // convert an array to a date. // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] function configFromArray (config) { var i, date, input = [], currentDate, yearToUse; if (config._d) { return; } currentDate = currentDateArray(config); //compute day of the year from weeks and weekdays if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { dayOfYearFromWeekInfo(config); } //if the day of the year is set, figure out what it is if (config._dayOfYear) { yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); if (config._dayOfYear > daysInYear(yearToUse)) { getParsingFlags(config)._overflowDayOfYear = true; } date = createUTCDate(yearToUse, 0, config._dayOfYear); config._a[MONTH] = date.getUTCMonth(); config._a[DATE] = date.getUTCDate(); } // Default to current date. // * if no year, month, day of month are given, default to today // * if day of month is given, default month and year // * if month is given, default only year // * if year is given, don't default anything for (i = 0; i < 3 && config._a[i] == null; ++i) { config._a[i] = input[i] = currentDate[i]; } // Zero out whatever was not defaulted, including time for (; i < 7; i++) { config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; } // Check for 24:00:00.000 if (config._a[HOUR] === 24 && config._a[MINUTE] === 0 && config._a[SECOND] === 0 && config._a[MILLISECOND] === 0) { config._nextDay = true; config._a[HOUR] = 0; } config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); // Apply timezone offset from input. The actual utcOffset can be changed // with parseZone. if (config._tzm != null) { config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); } if (config._nextDay) { config._a[HOUR] = 24; } } function dayOfYearFromWeekInfo(config) { var w, weekYear, week, weekday, dow, doy, temp; w = config._w; if (w.GG != null || w.W != null || w.E != null) { dow = 1; doy = 4; // TODO: We need to take the current isoWeekYear, but that depends on // how we interpret now (local, utc, fixed offset). So create // a now version of current config (take local/utc/offset flags, and // create now). weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); week = defaults(w.W, 1); weekday = defaults(w.E, 1); } else { dow = config._locale._week.dow; doy = config._locale._week.doy; weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); week = defaults(w.w, 1); if (w.d != null) { // weekday -- low day numbers are considered next week weekday = w.d; if (weekday < dow) { ++week; } } else if (w.e != null) { // local weekday -- counting starts from begining of week weekday = w.e + dow; } else { // default to begining of week weekday = dow; } } temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); config._a[YEAR] = temp.year; config._dayOfYear = temp.dayOfYear; } utils_hooks__hooks.ISO_8601 = function () {}; // date from string and format string function configFromStringAndFormat(config) { // TODO: Move this to another part of the creation flow to prevent circular deps if (config._f === utils_hooks__hooks.ISO_8601) { configFromISO(config); return; } config._a = []; getParsingFlags(config).empty = true; // This array is used to make a Date, either with `new Date` or `Date.UTC` var string = '' + config._i, i, parsedInput, tokens, token, skipped, stringLength = string.length, totalParsedInputLength = 0; tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; for (i = 0; i < tokens.length; i++) { token = tokens[i]; parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; if (parsedInput) { skipped = string.substr(0, string.indexOf(parsedInput)); if (skipped.length > 0) { getParsingFlags(config).unusedInput.push(skipped); } string = string.slice(string.indexOf(parsedInput) + parsedInput.length); totalParsedInputLength += parsedInput.length; } // don't parse if it's not a known token if (formatTokenFunctions[token]) { if (parsedInput) { getParsingFlags(config).empty = false; } else { getParsingFlags(config).unusedTokens.push(token); } addTimeToArrayFromToken(token, parsedInput, config); } else if (config._strict && !parsedInput) { getParsingFlags(config).unusedTokens.push(token); } } // add remaining unparsed input length to the string getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; if (string.length > 0) { getParsingFlags(config).unusedInput.push(string); } // clear _12h flag if hour is <= 12 if (getParsingFlags(config).bigHour === true && config._a[HOUR] <= 12 && config._a[HOUR] > 0) { getParsingFlags(config).bigHour = undefined; } // handle meridiem config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); configFromArray(config); checkOverflow(config); } function meridiemFixWrap (locale, hour, meridiem) { var isPm; if (meridiem == null) { // nothing to do return hour; } if (locale.meridiemHour != null) { return locale.meridiemHour(hour, meridiem); } else if (locale.isPM != null) { // Fallback isPm = locale.isPM(meridiem); if (isPm && hour < 12) { hour += 12; } if (!isPm && hour === 12) { hour = 0; } return hour; } else { // this is not supposed to happen return hour; } } function configFromStringAndArray(config) { var tempConfig, bestMoment, scoreToBeat, i, currentScore; if (config._f.length === 0) { getParsingFlags(config).invalidFormat = true; config._d = new Date(NaN); return; } for (i = 0; i < config._f.length; i++) { currentScore = 0; tempConfig = copyConfig({}, config); if (config._useUTC != null) { tempConfig._useUTC = config._useUTC; } tempConfig._f = config._f[i]; configFromStringAndFormat(tempConfig); if (!valid__isValid(tempConfig)) { continue; } // if there is any input that was not parsed add a penalty for that format currentScore += getParsingFlags(tempConfig).charsLeftOver; //or tokens currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; getParsingFlags(tempConfig).score = currentScore; if (scoreToBeat == null || currentScore < scoreToBeat) { scoreToBeat = currentScore; bestMoment = tempConfig; } } extend(config, bestMoment || tempConfig); } function configFromObject(config) { if (config._d) { return; } var i = normalizeObjectUnits(config._i); config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; configFromArray(config); } function createFromConfig (config) { var input = config._i, format = config._f, res; config._locale = config._locale || locale_locales__getLocale(config._l); if (input === null || (format === undefined && input === '')) { return valid__createInvalid({nullInput: true}); } if (typeof input === 'string') { config._i = input = config._locale.preparse(input); } if (isMoment(input)) { return new Moment(checkOverflow(input)); } else if (isArray(format)) { configFromStringAndArray(config); } else if (format) { configFromStringAndFormat(config); } else if (isDate(input)) { config._d = input; } else { configFromInput(config); } res = new Moment(checkOverflow(config)); if (res._nextDay) { // Adding is smart enough around DST res.add(1, 'd'); res._nextDay = undefined; } return res; } function configFromInput(config) { var input = config._i; if (input === undefined) { config._d = new Date(); } else if (isDate(input)) { config._d = new Date(+input); } else if (typeof input === 'string') { configFromString(config); } else if (isArray(input)) { config._a = map(input.slice(0), function (obj) { return parseInt(obj, 10); }); configFromArray(config); } else if (typeof(input) === 'object') { configFromObject(config); } else if (typeof(input) === 'number') { // from milliseconds config._d = new Date(input); } else { utils_hooks__hooks.createFromInputFallback(config); } } function createLocalOrUTC (input, format, locale, strict, isUTC) { var c = {}; if (typeof(locale) === 'boolean') { strict = locale; locale = undefined; } // object construction must be done this way. // https://github.com/moment/moment/issues/1423 c._isAMomentObject = true; c._useUTC = c._isUTC = isUTC; c._l = locale; c._i = input; c._f = format; c._strict = strict; return createFromConfig(c); } function local__createLocal (input, format, locale, strict) { return createLocalOrUTC(input, format, locale, strict, false); } var prototypeMin = deprecate( 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', function () { var other = local__createLocal.apply(null, arguments); return other < this ? this : other; } ); var prototypeMax = deprecate( 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', function () { var other = local__createLocal.apply(null, arguments); return other > this ? this : other; } ); // Pick a moment m from moments so that m[fn](other) is true for all // other. This relies on the function fn to be transitive. // // moments should either be an array of moment objects or an array, whose // first element is an array of moment objects. function pickBy(fn, moments) { var res, i; if (moments.length === 1 && isArray(moments[0])) { moments = moments[0]; } if (!moments.length) { return local__createLocal(); } res = moments[0]; for (i = 1; i < moments.length; ++i) { if (moments[i][fn](res)) { res = moments[i]; } } return res; } // TODO: Use [].sort instead? function min () { var args = [].slice.call(arguments, 0); return pickBy('isBefore', args); } function max () { var args = [].slice.call(arguments, 0); return pickBy('isAfter', args); } function Duration (duration) { var normalizedInput = normalizeObjectUnits(duration), years = normalizedInput.year || 0, quarters = normalizedInput.quarter || 0, months = normalizedInput.month || 0, weeks = normalizedInput.week || 0, days = normalizedInput.day || 0, hours = normalizedInput.hour || 0, minutes = normalizedInput.minute || 0, seconds = normalizedInput.second || 0, milliseconds = normalizedInput.millisecond || 0; // representation for dateAddRemove this._milliseconds = +milliseconds + seconds * 1e3 + // 1000 minutes * 6e4 + // 1000 * 60 hours * 36e5; // 1000 * 60 * 60 // Because of dateAddRemove treats 24 hours as different from a // day when working around DST, we need to store them separately this._days = +days + weeks * 7; // It is impossible translate months into days without knowing // which months you are are talking about, so we have to store // it separately. this._months = +months + quarters * 3 + years * 12; this._data = {}; this._locale = locale_locales__getLocale(); this._bubble(); } function isDuration (obj) { return obj instanceof Duration; } function offset (token, separator) { addFormatToken(token, 0, 0, function () { var offset = this.utcOffset(); var sign = '+'; if (offset < 0) { offset = -offset; sign = '-'; } return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); }); } offset('Z', ':'); offset('ZZ', ''); // PARSING addRegexToken('Z', matchOffset); addRegexToken('ZZ', matchOffset); addParseToken(['Z', 'ZZ'], function (input, array, config) { config._useUTC = true; config._tzm = offsetFromString(input); }); // HELPERS // timezone chunker // '+10:00' > ['10', '00'] // '-1530' > ['-15', '30'] var chunkOffset = /([\+\-]|\d\d)/gi; function offsetFromString(string) { var matches = ((string || '').match(matchOffset) || []); var chunk = matches[matches.length - 1] || []; var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; var minutes = +(parts[1] * 60) + toInt(parts[2]); return parts[0] === '+' ? minutes : -minutes; } // Return a moment from input, that is local/utc/zone equivalent to model. function cloneWithOffset(input, model) { var res, diff; if (model._isUTC) { res = model.clone(); diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); // Use low-level api, because this fn is low-level api. res._d.setTime(+res._d + diff); utils_hooks__hooks.updateOffset(res, false); return res; } else { return local__createLocal(input).local(); } return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); } function getDateOffset (m) { // On Firefox.24 Date#getTimezoneOffset returns a floating point. // https://github.com/moment/moment/pull/1871 return -Math.round(m._d.getTimezoneOffset() / 15) * 15; } // HOOKS // This function will be called whenever a moment is mutated. // It is intended to keep the offset in sync with the timezone. utils_hooks__hooks.updateOffset = function () {}; // MOMENTS // keepLocalTime = true means only change the timezone, without // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset // +0200, so we adjust the time as needed, to be valid. // // Keeping the time actually adds/subtracts (one hour) // from the actual represented time. That is why we call updateOffset // a second time. In case it wants us to change the offset again // _changeInProgress == true case, then we have to adjust, because // there is no such time in the given timezone. function getSetOffset (input, keepLocalTime) { var offset = this._offset || 0, localAdjust; if (input != null) { if (typeof input === 'string') { input = offsetFromString(input); } if (Math.abs(input) < 16) { input = input * 60; } if (!this._isUTC && keepLocalTime) { localAdjust = getDateOffset(this); } this._offset = input; this._isUTC = true; if (localAdjust != null) { this.add(localAdjust, 'm'); } if (offset !== input) { if (!keepLocalTime || this._changeInProgress) { add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); } else if (!this._changeInProgress) { this._changeInProgress = true; utils_hooks__hooks.updateOffset(this, true); this._changeInProgress = null; } } return this; } else { return this._isUTC ? offset : getDateOffset(this); } } function getSetZone (input, keepLocalTime) { if (input != null) { if (typeof input !== 'string') { input = -input; } this.utcOffset(input, keepLocalTime); return this; } else { return -this.utcOffset(); } } function setOffsetToUTC (keepLocalTime) { return this.utcOffset(0, keepLocalTime); } function setOffsetToLocal (keepLocalTime) { if (this._isUTC) { this.utcOffset(0, keepLocalTime); this._isUTC = false; if (keepLocalTime) { this.subtract(getDateOffset(this), 'm'); } } return this; } function setOffsetToParsedOffset () { if (this._tzm) { this.utcOffset(this._tzm); } else if (typeof this._i === 'string') { this.utcOffset(offsetFromString(this._i)); } return this; } function hasAlignedHourOffset (input) { if (!input) { input = 0; } else { input = local__createLocal(input).utcOffset(); } return (this.utcOffset() - input) % 60 === 0; } function isDaylightSavingTime () { return ( this.utcOffset() > this.clone().month(0).utcOffset() || this.utcOffset() > this.clone().month(5).utcOffset() ); } function isDaylightSavingTimeShifted () { if (this._a) { var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); return this.isValid() && compareArrays(this._a, other.toArray()) > 0; } return false; } function isLocal () { return !this._isUTC; } function isUtcOffset () { return this._isUTC; } function isUtc () { return this._isUTC && this._offset === 0; } var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; function create__createDuration (input, key) { var duration = input, // matching against regexp is expensive, do it on demand match = null, sign, ret, diffRes; if (isDuration(input)) { duration = { ms : input._milliseconds, d : input._days, M : input._months }; } else if (typeof input === 'number') { duration = {}; if (key) { duration[key] = input; } else { duration.milliseconds = input; } } else if (!!(match = aspNetRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; duration = { y : 0, d : toInt(match[DATE]) * sign, h : toInt(match[HOUR]) * sign, m : toInt(match[MINUTE]) * sign, s : toInt(match[SECOND]) * sign, ms : toInt(match[MILLISECOND]) * sign }; } else if (!!(match = create__isoRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; duration = { y : parseIso(match[2], sign), M : parseIso(match[3], sign), d : parseIso(match[4], sign), h : parseIso(match[5], sign), m : parseIso(match[6], sign), s : parseIso(match[7], sign), w : parseIso(match[8], sign) }; } else if (duration == null) {// checks for null or undefined duration = {}; } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); duration = {}; duration.ms = diffRes.milliseconds; duration.M = diffRes.months; } ret = new Duration(duration); if (isDuration(input) && hasOwnProp(input, '_locale')) { ret._locale = input._locale; } return ret; } create__createDuration.fn = Duration.prototype; function parseIso (inp, sign) { // We'd normally use ~~inp for this, but unfortunately it also // converts floats to ints. // inp may be undefined, so careful calling replace on it. var res = inp && parseFloat(inp.replace(',', '.')); // apply sign while we're at it return (isNaN(res) ? 0 : res) * sign; } function positiveMomentsDifference(base, other) { var res = {milliseconds: 0, months: 0}; res.months = other.month() - base.month() + (other.year() - base.year()) * 12; if (base.clone().add(res.months, 'M').isAfter(other)) { --res.months; } res.milliseconds = +other - +(base.clone().add(res.months, 'M')); return res; } function momentsDifference(base, other) { var res; other = cloneWithOffset(other, base); if (base.isBefore(other)) { res = positiveMomentsDifference(base, other); } else { res = positiveMomentsDifference(other, base); res.milliseconds = -res.milliseconds; res.months = -res.months; } return res; } function createAdder(direction, name) { return function (val, period) { var dur, tmp; //invert the arguments, but complain about it if (period !== null && !isNaN(+period)) { deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); tmp = val; val = period; period = tmp; } val = typeof val === 'string' ? +val : val; dur = create__createDuration(val, period); add_subtract__addSubtract(this, dur, direction); return this; }; } function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { var milliseconds = duration._milliseconds, days = duration._days, months = duration._months; updateOffset = updateOffset == null ? true : updateOffset; if (milliseconds) { mom._d.setTime(+mom._d + milliseconds * isAdding); } if (days) { get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); } if (months) { setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); } if (updateOffset) { utils_hooks__hooks.updateOffset(mom, days || months); } } var add_subtract__add = createAdder(1, 'add'); var add_subtract__subtract = createAdder(-1, 'subtract'); function moment_calendar__calendar (time) { // We want to compare the start of today, vs this. // Getting start-of-today depends on whether we're local/utc/offset or not. var now = time || local__createLocal(), sod = cloneWithOffset(now, this).startOf('day'), diff = this.diff(sod, 'days', true), format = diff < -6 ? 'sameElse' : diff < -1 ? 'lastWeek' : diff < 0 ? 'lastDay' : diff < 1 ? 'sameDay' : diff < 2 ? 'nextDay' : diff < 7 ? 'nextWeek' : 'sameElse'; return this.format(this.localeData().calendar(format, this, local__createLocal(now))); } function clone () { return new Moment(this); } function isAfter (input, units) { var inputMs; units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); if (units === 'millisecond') { input = isMoment(input) ? input : local__createLocal(input); return +this > +input; } else { inputMs = isMoment(input) ? +input : +local__createLocal(input); return inputMs < +this.clone().startOf(units); } } function isBefore (input, units) { var inputMs; units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); if (units === 'millisecond') { input = isMoment(input) ? input : local__createLocal(input); return +this < +input; } else { inputMs = isMoment(input) ? +input : +local__createLocal(input); return +this.clone().endOf(units) < inputMs; } } function isBetween (from, to, units) { return this.isAfter(from, units) && this.isBefore(to, units); } function isSame (input, units) { var inputMs; units = normalizeUnits(units || 'millisecond'); if (units === 'millisecond') { input = isMoment(input) ? input : local__createLocal(input); return +this === +input; } else { inputMs = +local__createLocal(input); return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); } } function absFloor (number) { if (number < 0) { return Math.ceil(number); } else { return Math.floor(number); } } function diff (input, units, asFloat) { var that = cloneWithOffset(input, this), zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, delta, output; units = normalizeUnits(units); if (units === 'year' || units === 'month' || units === 'quarter') { output = monthDiff(this, that); if (units === 'quarter') { output = output / 3; } else if (units === 'year') { output = output / 12; } } else { delta = this - that; output = units === 'second' ? delta / 1e3 : // 1000 units === 'minute' ? delta / 6e4 : // 1000 * 60 units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst delta; } return asFloat ? output : absFloor(output); } function monthDiff (a, b) { // difference in months var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), // b is in (anchor - 1 month, anchor + 1 month) anchor = a.clone().add(wholeMonthDiff, 'months'), anchor2, adjust; if (b - anchor < 0) { anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); // linear across the month adjust = (b - anchor) / (anchor - anchor2); } else { anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); // linear across the month adjust = (b - anchor) / (anchor2 - anchor); } return -(wholeMonthDiff + adjust); } utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; function toString () { return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); } function moment_format__toISOString () { var m = this.clone().utc(); if (0 < m.year() && m.year() <= 9999) { if ('function' === typeof Date.prototype.toISOString) { // native implementation is ~50x faster, use it when we can return this.toDate().toISOString(); } else { return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); } } else { return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); } } function format (inputString) { var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); return this.localeData().postformat(output); } function from (time, withoutSuffix) { if (!this.isValid()) { return this.localeData().invalidDate(); } return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); } function fromNow (withoutSuffix) { return this.from(local__createLocal(), withoutSuffix); } function to (time, withoutSuffix) { if (!this.isValid()) { return this.localeData().invalidDate(); } return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); } function toNow (withoutSuffix) { return this.to(local__createLocal(), withoutSuffix); } function locale (key) { var newLocaleData; if (key === undefined) { return this._locale._abbr; } else { newLocaleData = locale_locales__getLocale(key); if (newLocaleData != null) { this._locale = newLocaleData; } return this; } } var lang = deprecate( 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', function (key) { if (key === undefined) { return this.localeData(); } else { return this.locale(key); } } ); function localeData () { return this._locale; } function startOf (units) { units = normalizeUnits(units); // the following switch intentionally omits break keywords // to utilize falling through the cases. switch (units) { case 'year': this.month(0); /* falls through */ case 'quarter': case 'month': this.date(1); /* falls through */ case 'week': case 'isoWeek': case 'day': this.hours(0); /* falls through */ case 'hour': this.minutes(0); /* falls through */ case 'minute': this.seconds(0); /* falls through */ case 'second': this.milliseconds(0); } // weeks are a special case if (units === 'week') { this.weekday(0); } if (units === 'isoWeek') { this.isoWeekday(1); } // quarters are also special if (units === 'quarter') { this.month(Math.floor(this.month() / 3) * 3); } return this; } function endOf (units) { units = normalizeUnits(units); if (units === undefined || units === 'millisecond') { return this; } return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); } function to_type__valueOf () { return +this._d - ((this._offset || 0) * 60000); } function unix () { return Math.floor(+this / 1000); } function toDate () { return this._offset ? new Date(+this) : this._d; } function toArray () { var m = this; return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; } function moment_valid__isValid () { return valid__isValid(this); } function parsingFlags () { return extend({}, getParsingFlags(this)); } function invalidAt () { return getParsingFlags(this).overflow; } addFormatToken(0, ['gg', 2], 0, function () { return this.weekYear() % 100; }); addFormatToken(0, ['GG', 2], 0, function () { return this.isoWeekYear() % 100; }); function addWeekYearFormatToken (token, getter) { addFormatToken(0, [token, token.length], 0, getter); } addWeekYearFormatToken('gggg', 'weekYear'); addWeekYearFormatToken('ggggg', 'weekYear'); addWeekYearFormatToken('GGGG', 'isoWeekYear'); addWeekYearFormatToken('GGGGG', 'isoWeekYear'); // ALIASES addUnitAlias('weekYear', 'gg'); addUnitAlias('isoWeekYear', 'GG'); // PARSING addRegexToken('G', matchSigned); addRegexToken('g', matchSigned); addRegexToken('GG', match1to2, match2); addRegexToken('gg', match1to2, match2); addRegexToken('GGGG', match1to4, match4); addRegexToken('gggg', match1to4, match4); addRegexToken('GGGGG', match1to6, match6); addRegexToken('ggggg', match1to6, match6); addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { week[token.substr(0, 2)] = toInt(input); }); addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { week[token] = utils_hooks__hooks.parseTwoDigitYear(input); }); // HELPERS function weeksInYear(year, dow, doy) { return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; } // MOMENTS function getSetWeekYear (input) { var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; return input == null ? year : this.add((input - year), 'y'); } function getSetISOWeekYear (input) { var year = weekOfYear(this, 1, 4).year; return input == null ? year : this.add((input - year), 'y'); } function getISOWeeksInYear () { return weeksInYear(this.year(), 1, 4); } function getWeeksInYear () { var weekInfo = this.localeData()._week; return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); } addFormatToken('Q', 0, 0, 'quarter'); // ALIASES addUnitAlias('quarter', 'Q'); // PARSING addRegexToken('Q', match1); addParseToken('Q', function (input, array) { array[MONTH] = (toInt(input) - 1) * 3; }); // MOMENTS function getSetQuarter (input) { return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); } addFormatToken('D', ['DD', 2], 'Do', 'date'); // ALIASES addUnitAlias('date', 'D'); // PARSING addRegexToken('D', match1to2); addRegexToken('DD', match1to2, match2); addRegexToken('Do', function (isStrict, locale) { return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; }); addParseToken(['D', 'DD'], DATE); addParseToken('Do', function (input, array) { array[DATE] = toInt(input.match(match1to2)[0], 10); }); // MOMENTS var getSetDayOfMonth = makeGetSet('Date', true); addFormatToken('d', 0, 'do', 'day'); addFormatToken('dd', 0, 0, function (format) { return this.localeData().weekdaysMin(this, format); }); addFormatToken('ddd', 0, 0, function (format) { return this.localeData().weekdaysShort(this, format); }); addFormatToken('dddd', 0, 0, function (format) { return this.localeData().weekdays(this, format); }); addFormatToken('e', 0, 0, 'weekday'); addFormatToken('E', 0, 0, 'isoWeekday'); // ALIASES addUnitAlias('day', 'd'); addUnitAlias('weekday', 'e'); addUnitAlias('isoWeekday', 'E'); // PARSING addRegexToken('d', match1to2); addRegexToken('e', match1to2); addRegexToken('E', match1to2); addRegexToken('dd', matchWord); addRegexToken('ddd', matchWord); addRegexToken('dddd', matchWord); addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { var weekday = config._locale.weekdaysParse(input); // if we didn't get a weekday name, mark the date as invalid if (weekday != null) { week.d = weekday; } else { getParsingFlags(config).invalidWeekday = input; } }); addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { week[token] = toInt(input); }); // HELPERS function parseWeekday(input, locale) { if (typeof input === 'string') { if (!isNaN(input)) { input = parseInt(input, 10); } else { input = locale.weekdaysParse(input); if (typeof input !== 'number') { return null; } } } return input; } // LOCALES var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); function localeWeekdays (m) { return this._weekdays[m.day()]; } var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); function localeWeekdaysShort (m) { return this._weekdaysShort[m.day()]; } var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); function localeWeekdaysMin (m) { return this._weekdaysMin[m.day()]; } function localeWeekdaysParse (weekdayName) { var i, mom, regex; if (!this._weekdaysParse) { this._weekdaysParse = []; } for (i = 0; i < 7; i++) { // make the regex if we don't have it already if (!this._weekdaysParse[i]) { mom = local__createLocal([2000, 1]).day(i); regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); } // test the regex if (this._weekdaysParse[i].test(weekdayName)) { return i; } } } // MOMENTS function getSetDayOfWeek (input) { var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); if (input != null) { input = parseWeekday(input, this.localeData()); return this.add(input - day, 'd'); } else { return day; } } function getSetLocaleDayOfWeek (input) { var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; return input == null ? weekday : this.add(input - weekday, 'd'); } function getSetISODayOfWeek (input) { // behaves the same as moment#day except // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) // as a setter, sunday should belong to the previous week. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); } addFormatToken('H', ['HH', 2], 0, 'hour'); addFormatToken('h', ['hh', 2], 0, function () { return this.hours() % 12 || 12; }); function meridiem (token, lowercase) { addFormatToken(token, 0, 0, function () { return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); }); } meridiem('a', true); meridiem('A', false); // ALIASES addUnitAlias('hour', 'h'); // PARSING function matchMeridiem (isStrict, locale) { return locale._meridiemParse; } addRegexToken('a', matchMeridiem); addRegexToken('A', matchMeridiem); addRegexToken('H', match1to2); addRegexToken('h', match1to2); addRegexToken('HH', match1to2, match2); addRegexToken('hh', match1to2, match2); addParseToken(['H', 'HH'], HOUR); addParseToken(['a', 'A'], function (input, array, config) { config._isPm = config._locale.isPM(input); config._meridiem = input; }); addParseToken(['h', 'hh'], function (input, array, config) { array[HOUR] = toInt(input); getParsingFlags(config).bigHour = true; }); // LOCALES function localeIsPM (input) { // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays // Using charAt should be more compatible. return ((input + '').toLowerCase().charAt(0) === 'p'); } var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; function localeMeridiem (hours, minutes, isLower) { if (hours > 11) { return isLower ? 'pm' : 'PM'; } else { return isLower ? 'am' : 'AM'; } } // MOMENTS // Setting the hour should keep the time, because the user explicitly // specified which hour he wants. So trying to maintain the same hour (in // a new timezone) makes sense. Adding/subtracting hours does not follow // this rule. var getSetHour = makeGetSet('Hours', true); addFormatToken('m', ['mm', 2], 0, 'minute'); // ALIASES addUnitAlias('minute', 'm'); // PARSING addRegexToken('m', match1to2); addRegexToken('mm', match1to2, match2); addParseToken(['m', 'mm'], MINUTE); // MOMENTS var getSetMinute = makeGetSet('Minutes', false); addFormatToken('s', ['ss', 2], 0, 'second'); // ALIASES addUnitAlias('second', 's'); // PARSING addRegexToken('s', match1to2); addRegexToken('ss', match1to2, match2); addParseToken(['s', 'ss'], SECOND); // MOMENTS var getSetSecond = makeGetSet('Seconds', false); addFormatToken('S', 0, 0, function () { return ~~(this.millisecond() / 100); }); addFormatToken(0, ['SS', 2], 0, function () { return ~~(this.millisecond() / 10); }); function millisecond__milliseconds (token) { addFormatToken(0, [token, 3], 0, 'millisecond'); } millisecond__milliseconds('SSS'); millisecond__milliseconds('SSSS'); // ALIASES addUnitAlias('millisecond', 'ms'); // PARSING addRegexToken('S', match1to3, match1); addRegexToken('SS', match1to3, match2); addRegexToken('SSS', match1to3, match3); addRegexToken('SSSS', matchUnsigned); addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { array[MILLISECOND] = toInt(('0.' + input) * 1000); }); // MOMENTS var getSetMillisecond = makeGetSet('Milliseconds', false); addFormatToken('z', 0, 0, 'zoneAbbr'); addFormatToken('zz', 0, 0, 'zoneName'); // MOMENTS function getZoneAbbr () { return this._isUTC ? 'UTC' : ''; } function getZoneName () { return this._isUTC ? 'Coordinated Universal Time' : ''; } var momentPrototype__proto = Moment.prototype; momentPrototype__proto.add = add_subtract__add; momentPrototype__proto.calendar = moment_calendar__calendar; momentPrototype__proto.clone = clone; momentPrototype__proto.diff = diff; momentPrototype__proto.endOf = endOf; momentPrototype__proto.format = format; momentPrototype__proto.from = from; momentPrototype__proto.fromNow = fromNow; momentPrototype__proto.to = to; momentPrototype__proto.toNow = toNow; momentPrototype__proto.get = getSet; momentPrototype__proto.invalidAt = invalidAt; momentPrototype__proto.isAfter = isAfter; momentPrototype__proto.isBefore = isBefore; momentPrototype__proto.isBetween = isBetween; momentPrototype__proto.isSame = isSame; momentPrototype__proto.isValid = moment_valid__isValid; momentPrototype__proto.lang = lang; momentPrototype__proto.locale = locale; momentPrototype__proto.localeData = localeData; momentPrototype__proto.max = prototypeMax; momentPrototype__proto.min = prototypeMin; momentPrototype__proto.parsingFlags = parsingFlags; momentPrototype__proto.set = getSet; momentPrototype__proto.startOf = startOf; momentPrototype__proto.subtract = add_subtract__subtract; momentPrototype__proto.toArray = toArray; momentPrototype__proto.toDate = toDate; momentPrototype__proto.toISOString = moment_format__toISOString; momentPrototype__proto.toJSON = moment_format__toISOString; momentPrototype__proto.toString = toString; momentPrototype__proto.unix = unix; momentPrototype__proto.valueOf = to_type__valueOf; // Year momentPrototype__proto.year = getSetYear; momentPrototype__proto.isLeapYear = getIsLeapYear; // Week Year momentPrototype__proto.weekYear = getSetWeekYear; momentPrototype__proto.isoWeekYear = getSetISOWeekYear; // Quarter momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; // Month momentPrototype__proto.month = getSetMonth; momentPrototype__proto.daysInMonth = getDaysInMonth; // Week momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; momentPrototype__proto.weeksInYear = getWeeksInYear; momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; // Day momentPrototype__proto.date = getSetDayOfMonth; momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; momentPrototype__proto.weekday = getSetLocaleDayOfWeek; momentPrototype__proto.isoWeekday = getSetISODayOfWeek; momentPrototype__proto.dayOfYear = getSetDayOfYear; // Hour momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; // Minute momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; // Second momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; // Millisecond momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; // Offset momentPrototype__proto.utcOffset = getSetOffset; momentPrototype__proto.utc = setOffsetToUTC; momentPrototype__proto.local = setOffsetToLocal; momentPrototype__proto.parseZone = setOffsetToParsedOffset; momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; momentPrototype__proto.isDST = isDaylightSavingTime; momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; momentPrototype__proto.isLocal = isLocal; momentPrototype__proto.isUtcOffset = isUtcOffset; momentPrototype__proto.isUtc = isUtc; momentPrototype__proto.isUTC = isUtc; // Timezone momentPrototype__proto.zoneAbbr = getZoneAbbr; momentPrototype__proto.zoneName = getZoneName; // Deprecations momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); var momentPrototype = momentPrototype__proto; function moment__createUnix (input) { return local__createLocal(input * 1000); } function moment__createInZone () { return local__createLocal.apply(null, arguments).parseZone(); } var defaultCalendar = { sameDay : '[Today at] LT', nextDay : '[Tomorrow at] LT', nextWeek : 'dddd [at] LT', lastDay : '[Yesterday at] LT', lastWeek : '[Last] dddd [at] LT', sameElse : 'L' }; function locale_calendar__calendar (key, mom, now) { var output = this._calendar[key]; return typeof output === 'function' ? output.call(mom, now) : output; } var defaultLongDateFormat = { LTS : 'h:mm:ss A', LT : 'h:mm A', L : 'MM/DD/YYYY', LL : 'MMMM D, YYYY', LLL : 'MMMM D, YYYY LT', LLLL : 'dddd, MMMM D, YYYY LT' }; function longDateFormat (key) { var output = this._longDateFormat[key]; if (!output && this._longDateFormat[key.toUpperCase()]) { output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { return val.slice(1); }); this._longDateFormat[key] = output; } return output; } var defaultInvalidDate = 'Invalid date'; function invalidDate () { return this._invalidDate; } var defaultOrdinal = '%d'; var defaultOrdinalParse = /\d{1,2}/; function ordinal (number) { return this._ordinal.replace('%d', number); } function preParsePostFormat (string) { return string; } var defaultRelativeTime = { future : 'in %s', past : '%s ago', s : 'a few seconds', m : 'a minute', mm : '%d minutes', h : 'an hour', hh : '%d hours', d : 'a day', dd : '%d days', M : 'a month', MM : '%d months', y : 'a year', yy : '%d years' }; function relative__relativeTime (number, withoutSuffix, string, isFuture) { var output = this._relativeTime[string]; return (typeof output === 'function') ? output(number, withoutSuffix, string, isFuture) : output.replace(/%d/i, number); } function pastFuture (diff, output) { var format = this._relativeTime[diff > 0 ? 'future' : 'past']; return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); } function locale_set__set (config) { var prop, i; for (i in config) { prop = config[i]; if (typeof prop === 'function') { this[i] = prop; } else { this['_' + i] = prop; } } // Lenient ordinal parsing accepts just a number in addition to // number + (possibly) stuff coming from _ordinalParseLenient. this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); } var prototype__proto = Locale.prototype; prototype__proto._calendar = defaultCalendar; prototype__proto.calendar = locale_calendar__calendar; prototype__proto._longDateFormat = defaultLongDateFormat; prototype__proto.longDateFormat = longDateFormat; prototype__proto._invalidDate = defaultInvalidDate; prototype__proto.invalidDate = invalidDate; prototype__proto._ordinal = defaultOrdinal; prototype__proto.ordinal = ordinal; prototype__proto._ordinalParse = defaultOrdinalParse; prototype__proto.preparse = preParsePostFormat; prototype__proto.postformat = preParsePostFormat; prototype__proto._relativeTime = defaultRelativeTime; prototype__proto.relativeTime = relative__relativeTime; prototype__proto.pastFuture = pastFuture; prototype__proto.set = locale_set__set; // Month prototype__proto.months = localeMonths; prototype__proto._months = defaultLocaleMonths; prototype__proto.monthsShort = localeMonthsShort; prototype__proto._monthsShort = defaultLocaleMonthsShort; prototype__proto.monthsParse = localeMonthsParse; // Week prototype__proto.week = localeWeek; prototype__proto._week = defaultLocaleWeek; prototype__proto.firstDayOfYear = localeFirstDayOfYear; prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; // Day of Week prototype__proto.weekdays = localeWeekdays; prototype__proto._weekdays = defaultLocaleWeekdays; prototype__proto.weekdaysMin = localeWeekdaysMin; prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; prototype__proto.weekdaysShort = localeWeekdaysShort; prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; prototype__proto.weekdaysParse = localeWeekdaysParse; // Hours prototype__proto.isPM = localeIsPM; prototype__proto._meridiemParse = defaultLocaleMeridiemParse; prototype__proto.meridiem = localeMeridiem; function lists__get (format, index, field, setter) { var locale = locale_locales__getLocale(); var utc = create_utc__createUTC().set(setter, index); return locale[field](utc, format); } function list (format, index, field, count, setter) { if (typeof format === 'number') { index = format; format = undefined; } format = format || ''; if (index != null) { return lists__get(format, index, field, setter); } var i; var out = []; for (i = 0; i < count; i++) { out[i] = lists__get(format, i, field, setter); } return out; } function lists__listMonths (format, index) { return list(format, index, 'months', 12, 'month'); } function lists__listMonthsShort (format, index) { return list(format, index, 'monthsShort', 12, 'month'); } function lists__listWeekdays (format, index) { return list(format, index, 'weekdays', 7, 'day'); } function lists__listWeekdaysShort (format, index) { return list(format, index, 'weekdaysShort', 7, 'day'); } function lists__listWeekdaysMin (format, index) { return list(format, index, 'weekdaysMin', 7, 'day'); } locale_locales__getSetGlobalLocale('en', { ordinalParse: /\d{1,2}(th|st|nd|rd)/, ordinal : function (number) { var b = number % 10, output = (toInt(number % 100 / 10) === 1) ? 'th' : (b === 1) ? 'st' : (b === 2) ? 'nd' : (b === 3) ? 'rd' : 'th'; return number + output; } }); // Side effect imports utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); var mathAbs = Math.abs; function duration_abs__abs () { var data = this._data; this._milliseconds = mathAbs(this._milliseconds); this._days = mathAbs(this._days); this._months = mathAbs(this._months); data.milliseconds = mathAbs(data.milliseconds); data.seconds = mathAbs(data.seconds); data.minutes = mathAbs(data.minutes); data.hours = mathAbs(data.hours); data.months = mathAbs(data.months); data.years = mathAbs(data.years); return this; } function duration_add_subtract__addSubtract (duration, input, value, direction) { var other = create__createDuration(input, value); duration._milliseconds += direction * other._milliseconds; duration._days += direction * other._days; duration._months += direction * other._months; return duration._bubble(); } // supports only 2.0-style add(1, 's') or add(duration) function duration_add_subtract__add (input, value) { return duration_add_subtract__addSubtract(this, input, value, 1); } // supports only 2.0-style subtract(1, 's') or subtract(duration) function duration_add_subtract__subtract (input, value) { return duration_add_subtract__addSubtract(this, input, value, -1); } function bubble () { var milliseconds = this._milliseconds; var days = this._days; var months = this._months; var data = this._data; var seconds, minutes, hours, years = 0; // The following code bubbles up values, see the tests for // examples of what that means. data.milliseconds = milliseconds % 1000; seconds = absFloor(milliseconds / 1000); data.seconds = seconds % 60; minutes = absFloor(seconds / 60); data.minutes = minutes % 60; hours = absFloor(minutes / 60); data.hours = hours % 24; days += absFloor(hours / 24); // Accurately convert days to years, assume start from year 0. years = absFloor(daysToYears(days)); days -= absFloor(yearsToDays(years)); // 30 days to a month // TODO (iskren): Use anchor date (like 1st Jan) to compute this. months += absFloor(days / 30); days %= 30; // 12 months -> 1 year years += absFloor(months / 12); months %= 12; data.days = days; data.months = months; data.years = years; return this; } function daysToYears (days) { // 400 years have 146097 days (taking into account leap year rules) return days * 400 / 146097; } function yearsToDays (years) { // years * 365 + absFloor(years / 4) - // absFloor(years / 100) + absFloor(years / 400); return years * 146097 / 400; } function as (units) { var days; var months; var milliseconds = this._milliseconds; units = normalizeUnits(units); if (units === 'month' || units === 'year') { days = this._days + milliseconds / 864e5; months = this._months + daysToYears(days) * 12; return units === 'month' ? months : months / 12; } else { // handle milliseconds separately because of floating point math errors (issue #1867) days = this._days + Math.round(yearsToDays(this._months / 12)); switch (units) { case 'week' : return days / 7 + milliseconds / 6048e5; case 'day' : return days + milliseconds / 864e5; case 'hour' : return days * 24 + milliseconds / 36e5; case 'minute' : return days * 1440 + milliseconds / 6e4; case 'second' : return days * 86400 + milliseconds / 1000; // Math.floor prevents floating point math errors here case 'millisecond': return Math.floor(days * 864e5) + milliseconds; default: throw new Error('Unknown unit ' + units); } } } // TODO: Use this.as('ms')? function duration_as__valueOf () { return ( this._milliseconds + this._days * 864e5 + (this._months % 12) * 2592e6 + toInt(this._months / 12) * 31536e6 ); } function makeAs (alias) { return function () { return this.as(alias); }; } var asMilliseconds = makeAs('ms'); var asSeconds = makeAs('s'); var asMinutes = makeAs('m'); var asHours = makeAs('h'); var asDays = makeAs('d'); var asWeeks = makeAs('w'); var asMonths = makeAs('M'); var asYears = makeAs('y'); function duration_get__get (units) { units = normalizeUnits(units); return this[units + 's'](); } function makeGetter(name) { return function () { return this._data[name]; }; } var duration_get__milliseconds = makeGetter('milliseconds'); var seconds = makeGetter('seconds'); var minutes = makeGetter('minutes'); var hours = makeGetter('hours'); var days = makeGetter('days'); var months = makeGetter('months'); var years = makeGetter('years'); function weeks () { return absFloor(this.days() / 7); } var round = Math.round; var thresholds = { s: 45, // seconds to minute m: 45, // minutes to hour h: 22, // hours to day d: 26, // days to month M: 11 // months to year }; // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); } function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { var duration = create__createDuration(posNegDuration).abs(); var seconds = round(duration.as('s')); var minutes = round(duration.as('m')); var hours = round(duration.as('h')); var days = round(duration.as('d')); var months = round(duration.as('M')); var years = round(duration.as('y')); var a = seconds < thresholds.s && ['s', seconds] || minutes === 1 && ['m'] || minutes < thresholds.m && ['mm', minutes] || hours === 1 && ['h'] || hours < thresholds.h && ['hh', hours] || days === 1 && ['d'] || days < thresholds.d && ['dd', days] || months === 1 && ['M'] || months < thresholds.M && ['MM', months] || years === 1 && ['y'] || ['yy', years]; a[2] = withoutSuffix; a[3] = +posNegDuration > 0; a[4] = locale; return substituteTimeAgo.apply(null, a); } // This function allows you to set a threshold for relative time strings function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { if (thresholds[threshold] === undefined) { return false; } if (limit === undefined) { return thresholds[threshold]; } thresholds[threshold] = limit; return true; } function humanize (withSuffix) { var locale = this.localeData(); var output = duration_humanize__relativeTime(this, !withSuffix, locale); if (withSuffix) { output = locale.pastFuture(+this, output); } return locale.postformat(output); } var iso_string__abs = Math.abs; function iso_string__toISOString() { // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js var Y = iso_string__abs(this.years()); var M = iso_string__abs(this.months()); var D = iso_string__abs(this.days()); var h = iso_string__abs(this.hours()); var m = iso_string__abs(this.minutes()); var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); var total = this.asSeconds(); if (!total) { // this is the same as C#'s (Noda) and python (isodate)... // but not other JS (goog.date) return 'P0D'; } return (total < 0 ? '-' : '') + 'P' + (Y ? Y + 'Y' : '') + (M ? M + 'M' : '') + (D ? D + 'D' : '') + ((h || m || s) ? 'T' : '') + (h ? h + 'H' : '') + (m ? m + 'M' : '') + (s ? s + 'S' : ''); } var duration_prototype__proto = Duration.prototype; duration_prototype__proto.abs = duration_abs__abs; duration_prototype__proto.add = duration_add_subtract__add; duration_prototype__proto.subtract = duration_add_subtract__subtract; duration_prototype__proto.as = as; duration_prototype__proto.asMilliseconds = asMilliseconds; duration_prototype__proto.asSeconds = asSeconds; duration_prototype__proto.asMinutes = asMinutes; duration_prototype__proto.asHours = asHours; duration_prototype__proto.asDays = asDays; duration_prototype__proto.asWeeks = asWeeks; duration_prototype__proto.asMonths = asMonths; duration_prototype__proto.asYears = asYears; duration_prototype__proto.valueOf = duration_as__valueOf; duration_prototype__proto._bubble = bubble; duration_prototype__proto.get = duration_get__get; duration_prototype__proto.milliseconds = duration_get__milliseconds; duration_prototype__proto.seconds = seconds; duration_prototype__proto.minutes = minutes; duration_prototype__proto.hours = hours; duration_prototype__proto.days = days; duration_prototype__proto.weeks = weeks; duration_prototype__proto.months = months; duration_prototype__proto.years = years; duration_prototype__proto.humanize = humanize; duration_prototype__proto.toISOString = iso_string__toISOString; duration_prototype__proto.toString = iso_string__toISOString; duration_prototype__proto.toJSON = iso_string__toISOString; duration_prototype__proto.locale = locale; duration_prototype__proto.localeData = localeData; // Deprecations duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); duration_prototype__proto.lang = lang; // Side effect imports addFormatToken('X', 0, 0, 'unix'); addFormatToken('x', 0, 0, 'valueOf'); // PARSING addRegexToken('x', matchSigned); addRegexToken('X', matchTimestamp); addParseToken('X', function (input, array, config) { config._d = new Date(parseFloat(input, 10) * 1000); }); addParseToken('x', function (input, array, config) { config._d = new Date(toInt(input)); }); // Side effect imports utils_hooks__hooks.version = '2.10.3'; setHookCallback(local__createLocal); utils_hooks__hooks.fn = momentPrototype; utils_hooks__hooks.min = min; utils_hooks__hooks.max = max; utils_hooks__hooks.utc = create_utc__createUTC; utils_hooks__hooks.unix = moment__createUnix; utils_hooks__hooks.months = lists__listMonths; utils_hooks__hooks.isDate = isDate; utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; utils_hooks__hooks.invalid = valid__createInvalid; utils_hooks__hooks.duration = create__createDuration; utils_hooks__hooks.isMoment = isMoment; utils_hooks__hooks.weekdays = lists__listWeekdays; utils_hooks__hooks.parseZone = moment__createInZone; utils_hooks__hooks.localeData = locale_locales__getLocale; utils_hooks__hooks.isDuration = isDuration; utils_hooks__hooks.monthsShort = lists__listMonthsShort; utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; utils_hooks__hooks.defineLocale = defineLocale; utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; utils_hooks__hooks.normalizeUnits = normalizeUnits; utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; var _moment = utils_hooks__hooks; return _moment; })); /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)(module))) /***/ }, /* 4 */ /***/ function(module, exports) { module.exports = function(module) { if(!module.webpackPolyfill) { module.deprecate = function() {}; module.paths = []; // module.parent = undefined by default module.children = []; module.webpackPolyfill = 1; } return module; } /***/ }, /* 5 */ /***/ function(module, exports) { function webpackContext(req) { throw new Error("Cannot find module '" + req + "'."); } webpackContext.keys = function() { return []; }; webpackContext.resolve = webpackContext; module.exports = webpackContext; webpackContext.id = 5; /***/ }, /* 6 */ /***/ function(module, exports) { /* WEBPACK VAR INJECTION */(function(global) {'use strict'; var _rng; var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; if (globalVar && globalVar.crypto && crypto.getRandomValues) { // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto // Moderately fast, high quality var _rnds8 = new Uint8Array(16); _rng = function whatwgRNG() { crypto.getRandomValues(_rnds8); return _rnds8; }; } if (!_rng) { // Math.random()-based (RNG) // // If all else fails, use Math.random(). It's fast, but is of unspecified // quality. var _rnds = new Array(16); _rng = function () { for (var i = 0, r; i < 16; i++) { if ((i & 3) === 0) r = Math.random() * 4294967296; _rnds[i] = r >>> ((i & 3) << 3) & 255; } return _rnds; }; } // uuid.js // // Copyright (c) 2010-2012 Robert Kieffer // MIT License - http://opensource.org/licenses/mit-license.php // Unique ID creation requires a high quality random # generator. We feature // detect to determine the best RNG source, normalizing to a function that // returns 128-bits of randomness, since that's what's usually required //var _rng = require('./rng'); // Maps for number <-> hex string conversion var _byteToHex = []; var _hexToByte = {}; for (var i = 0; i < 256; i++) { _byteToHex[i] = (i + 256).toString(16).substr(1); _hexToByte[_byteToHex[i]] = i; } // **`parse()` - Parse a UUID into it's component bytes** function parse(s, buf, offset) { var i = buf && offset || 0, ii = 0; buf = buf || []; s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { if (ii < 16) { // Don't overflow! buf[i + ii++] = _hexToByte[oct]; } }); // Zero out remaining bytes if string was short while (ii < 16) { buf[i + ii++] = 0; } return buf; } // **`unparse()` - Convert UUID byte array (ala parse()) into a string** function unparse(buf, offset) { var i = offset || 0, bth = _byteToHex; return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]]; } // **`v1()` - Generate time-based UUID** // // Inspired by https://github.com/LiosK/UUID.js // and http://docs.python.org/library/uuid.html // random #'s we need to init node and clockseq var _seedBytes = _rng(); // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) var _nodeId = [_seedBytes[0] | 1, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]]; // Per 4.2.2, randomize (14 bit) clockseq var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; // Previous uuid creation time var _lastMSecs = 0, _lastNSecs = 0; // See https://github.com/broofa/node-uuid for API details function v1(options, buf, offset) { var i = buf && offset || 0; var b = buf || []; options = options || {}; var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // UUID timestamps are 100 nano-second units since the Gregorian epoch, // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); // Per 4.2.1.2, use count of uuid's generated during the current clock // cycle to simulate higher resolution clock var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs) var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression if (dt < 0 && options.clockseq === undefined) { clockseq = clockseq + 1 & 16383; } // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new // time interval if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { nsecs = 0; } // Per 4.2.1.2 Throw error if too many uuids are requested if (nsecs >= 10000) { throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); } _lastMSecs = msecs; _lastNSecs = nsecs; _clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch msecs += 12219292800000; // `time_low` var tl = ((msecs & 268435455) * 10000 + nsecs) % 4294967296; b[i++] = tl >>> 24 & 255; b[i++] = tl >>> 16 & 255; b[i++] = tl >>> 8 & 255; b[i++] = tl & 255; // `time_mid` var tmh = msecs / 4294967296 * 10000 & 268435455; b[i++] = tmh >>> 8 & 255; b[i++] = tmh & 255; // `time_high_and_version` b[i++] = tmh >>> 24 & 15 | 16; // include version b[i++] = tmh >>> 16 & 255; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) b[i++] = clockseq >>> 8 | 128; // `clock_seq_low` b[i++] = clockseq & 255; // `node` var node = options.node || _nodeId; for (var n = 0; n < 6; n++) { b[i + n] = node[n]; } return buf ? buf : unparse(b); } // **`v4()` - Generate random UUID** // See https://github.com/broofa/node-uuid for API details function v4(options, buf, offset) { // Deprecated - 'format' argument, as supported in v1.2 var i = buf && offset || 0; if (typeof options == 'string') { buf = options == 'binary' ? new Array(16) : null; options = null; } options = options || {}; var rnds = options.random || (options.rng || _rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = rnds[6] & 15 | 64; rnds[8] = rnds[8] & 63 | 128; // Copy bytes to buffer, if provided if (buf) { for (var ii = 0; ii < 16; ii++) { buf[i + ii] = rnds[ii]; } } return buf || unparse(rnds); } // Export public API var uuid = v4; uuid.v1 = v1; uuid.v4 = v4; uuid.parse = parse; uuid.unparse = unparse; module.exports = uuid; /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, /* 7 */ /***/ function(module, exports) { // DOM utility methods /** * this prepares the JSON container for allocating SVG elements * @param JSONcontainer * @private */ 'use strict'; exports.prepareElements = function (JSONcontainer) { // cleanup the redundant svgElements; for (var elementType in JSONcontainer) { if (JSONcontainer.hasOwnProperty(elementType)) { JSONcontainer[elementType].redundant = JSONcontainer[elementType].used; JSONcontainer[elementType].used = []; } } }; /** * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from * which to remove the redundant elements. * * @param JSONcontainer * @private */ exports.cleanupElements = function (JSONcontainer) { // cleanup the redundant svgElements; for (var elementType in JSONcontainer) { if (JSONcontainer.hasOwnProperty(elementType)) { if (JSONcontainer[elementType].redundant) { for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) { JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]); } JSONcontainer[elementType].redundant = []; } } } }; /** * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. * * @param elementType * @param JSONcontainer * @param svgContainer * @returns {*} * @private */ exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) { var element; // allocate SVG element, if it doesnt yet exist, create one. if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before // check if there is an redundant element if (JSONcontainer[elementType].redundant.length > 0) { element = JSONcontainer[elementType].redundant[0]; JSONcontainer[elementType].redundant.shift(); } else { // create a new element and add it to the SVG element = document.createElementNS('http://www.w3.org/2000/svg', elementType); svgContainer.appendChild(element); } } else { // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. element = document.createElementNS('http://www.w3.org/2000/svg', elementType); JSONcontainer[elementType] = { used: [], redundant: [] }; svgContainer.appendChild(element); } JSONcontainer[elementType].used.push(element); return element; }; /** * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. * * @param elementType * @param JSONcontainer * @param DOMContainer * @returns {*} * @private */ exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { var element; // allocate DOM element, if it doesnt yet exist, create one. if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before // check if there is an redundant element if (JSONcontainer[elementType].redundant.length > 0) { element = JSONcontainer[elementType].redundant[0]; JSONcontainer[elementType].redundant.shift(); } else { // create a new element and add it to the SVG element = document.createElement(elementType); if (insertBefore !== undefined) { DOMContainer.insertBefore(element, insertBefore); } else { DOMContainer.appendChild(element); } } } else { // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. element = document.createElement(elementType); JSONcontainer[elementType] = { used: [], redundant: [] }; if (insertBefore !== undefined) { DOMContainer.insertBefore(element, insertBefore); } else { DOMContainer.appendChild(element); } } JSONcontainer[elementType].used.push(element); return element; }; /** * draw a point object. this is a seperate function because it can also be called by the legend. * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions * as well. * * @param x * @param y * @param group * @param JSONcontainer * @param svgContainer * @param labelObj * @returns {*} */ exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { var point; if (group.options.drawPoints.style == 'circle') { point = exports.getSVGElement('circle', JSONcontainer, svgContainer); point.setAttributeNS(null, 'cx', x); point.setAttributeNS(null, 'cy', y); point.setAttributeNS(null, 'r', 0.5 * group.options.drawPoints.size); } else { point = exports.getSVGElement('rect', JSONcontainer, svgContainer); point.setAttributeNS(null, 'x', x - 0.5 * group.options.drawPoints.size); point.setAttributeNS(null, 'y', y - 0.5 * group.options.drawPoints.size); point.setAttributeNS(null, 'width', group.options.drawPoints.size); point.setAttributeNS(null, 'height', group.options.drawPoints.size); } if (group.options.drawPoints.styles !== undefined) { point.setAttributeNS(null, 'style', group.group.options.drawPoints.styles); } point.setAttributeNS(null, 'class', group.className + ' vis-point'); //handle label if (labelObj) { var label = exports.getSVGElement('text', JSONcontainer, svgContainer); if (labelObj.xOffset) { x = x + labelObj.xOffset; } if (labelObj.yOffset) { y = y + labelObj.yOffset; } if (labelObj.content) { label.textContent = labelObj.content; } if (labelObj.className) { label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); } label.setAttributeNS(null, 'x', x); label.setAttributeNS(null, 'y', y); } return point; }; /** * draw a bar SVG element centered on the X coordinate * * @param x * @param y * @param className */ exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) { if (height != 0) { if (height < 0) { height *= -1; y -= height; } var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer); rect.setAttributeNS(null, 'x', x - 0.5 * width); rect.setAttributeNS(null, 'y', y); rect.setAttributeNS(null, 'width', width); rect.setAttributeNS(null, 'height', height); rect.setAttributeNS(null, 'class', className); if (style) { rect.setAttributeNS(null, 'style', style); } } }; /***/ }, /* 8 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var Queue = __webpack_require__(9); /** * DataSet * * Usage: * var dataSet = new DataSet({ * fieldId: '_id', * type: { * // ... * } * }); * * dataSet.add(item); * dataSet.add(data); * dataSet.update(item); * dataSet.update(data); * dataSet.remove(id); * dataSet.remove(ids); * var data = dataSet.get(); * var data = dataSet.get(id); * var data = dataSet.get(ids); * var data = dataSet.get(ids, options, data); * dataSet.clear(); * * A data set can: * - add/remove/update data * - gives triggers upon changes in the data * - can import/export data in various data formats * * @param {Array} [data] Optional array with initial data * @param {Object} [options] Available options: * {String} fieldId Field name of the id in the * items, 'id' by default. * {Object.} [type] * {String[]} [fields] field names to be returned * {function} [filter] filter items * {String | function} [order] Order the items by a field name or custom sort function. * @throws Error */ DataSet.prototype.get = function (args) { var me = this; // parse the arguments var id, ids, options; var firstType = util.getType(arguments[0]); if (firstType == 'String' || firstType == 'Number') { // get(id [, options]) id = arguments[0]; options = arguments[1]; } else if (firstType == 'Array') { // get(ids [, options]) ids = arguments[0]; options = arguments[1]; } else { // get([, options]) options = arguments[0]; } // determine the return type var returnType; if (options && options.returnType) { var allowedValues = ['Array', 'Object']; returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType; } else { returnType = 'Array'; } // build options var type = options && options.type || this._options.type; var filter = options && options.filter; var items = [], item, itemId, i, len; // convert items if (id != undefined) { // return a single item item = me._getItem(id, type); if (filter && !filter(item)) { item = null; } } else if (ids != undefined) { // return a subset of items for (i = 0, len = ids.length; i < len; i++) { item = me._getItem(ids[i], type); if (!filter || filter(item)) { items.push(item); } } } else { // return all items for (itemId in this._data) { if (this._data.hasOwnProperty(itemId)) { item = me._getItem(itemId, type); if (!filter || filter(item)) { items.push(item); } } } } // order the results if (options && options.order && id == undefined) { this._sort(items, options.order); } // filter fields of the items if (options && options.fields) { var fields = options.fields; if (id != undefined) { item = this._filterFields(item, fields); } else { for (i = 0, len = items.length; i < len; i++) { items[i] = this._filterFields(items[i], fields); } } } // return the results if (returnType == 'Object') { var result = {}; for (i = 0; i < items.length; i++) { result[items[i].id] = items[i]; } return result; } else { if (id != undefined) { // a single item return item; } else { // just return our array return items; } } }; /** * Get ids of all items or from a filtered set of items. * @param {Object} [options] An Object with options. Available options: * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Array} ids */ DataSet.prototype.getIds = function (options) { var data = this._data, filter = options && options.filter, order = options && options.order, type = options && options.type || this._options.type, i, len, id, item, items, ids = []; if (filter) { // get filtered items if (order) { // create ordered list items = []; for (id in data) { if (data.hasOwnProperty(id)) { item = this._getItem(id, type); if (filter(item)) { items.push(item); } } } this._sort(items, order); for (i = 0, len = items.length; i < len; i++) { ids[i] = items[i][this._fieldId]; } } else { // create unordered list for (id in data) { if (data.hasOwnProperty(id)) { item = this._getItem(id, type); if (filter(item)) { ids.push(item[this._fieldId]); } } } } } else { // get all items if (order) { // create an ordered list items = []; for (id in data) { if (data.hasOwnProperty(id)) { items.push(data[id]); } } this._sort(items, order); for (i = 0, len = items.length; i < len; i++) { ids[i] = items[i][this._fieldId]; } } else { // create unordered list for (id in data) { if (data.hasOwnProperty(id)) { item = data[id]; ids.push(item[this._fieldId]); } } } } return ids; }; /** * Returns the DataSet itself. Is overwritten for example by the DataView, * which returns the DataSet it is connected to instead. */ DataSet.prototype.getDataSet = function () { return this; }; /** * Execute a callback function for every item in the dataset. * @param {function} callback * @param {Object} [options] Available options: * {Object.} [type] * {String[]} [fields] filter fields * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. */ DataSet.prototype.forEach = function (callback, options) { var filter = options && options.filter, type = options && options.type || this._options.type, data = this._data, item, id; if (options && options.order) { // execute forEach on ordered list var items = this.get(options); for (var i = 0, len = items.length; i < len; i++) { item = items[i]; id = item[this._fieldId]; callback(item, id); } } else { // unordered for (id in data) { if (data.hasOwnProperty(id)) { item = this._getItem(id, type); if (!filter || filter(item)) { callback(item, id); } } } } }; /** * Map every item in the dataset. * @param {function} callback * @param {Object} [options] Available options: * {Object.} [type] * {String[]} [fields] filter fields * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Object[]} mappedItems */ DataSet.prototype.map = function (callback, options) { var filter = options && options.filter, type = options && options.type || this._options.type, mappedItems = [], data = this._data, item; // convert and filter items for (var id in data) { if (data.hasOwnProperty(id)) { item = this._getItem(id, type); if (!filter || filter(item)) { mappedItems.push(callback(item, id)); } } } // order items if (options && options.order) { this._sort(mappedItems, options.order); } return mappedItems; }; /** * Filter the fields of an item * @param {Object | null} item * @param {String[]} fields Field names * @return {Object | null} filteredItem or null if no item is provided * @private */ DataSet.prototype._filterFields = function (item, fields) { if (!item) { // item is null return item; } var filteredItem = {}; if (Array.isArray(fields)) { for (var field in item) { if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { filteredItem[field] = item[field]; } } } else { for (var field in item) { if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { filteredItem[fields[field]] = item[field]; } } } return filteredItem; }; /** * Sort the provided array with items * @param {Object[]} items * @param {String | function} order A field name or custom sort function. * @private */ DataSet.prototype._sort = function (items, order) { if (util.isString(order)) { // order by provided field name var name = order; // field name items.sort(function (a, b) { var av = a[name]; var bv = b[name]; return av > bv ? 1 : av < bv ? -1 : 0; }); } else if (typeof order === 'function') { // order by sort function items.sort(order); } // TODO: extend order by an Object {field:String, direction:String} // where direction can be 'asc' or 'desc' else { throw new TypeError('Order must be a function or a string'); } }; /** * Remove an object by pointer or by id * @param {String | Number | Object | Array} id Object or id, or an array with * objects or ids to be removed * @param {String} [senderId] Optional sender id * @return {Array} removedIds */ DataSet.prototype.remove = function (id, senderId) { var removedIds = [], i, len, removedId; if (Array.isArray(id)) { for (i = 0, len = id.length; i < len; i++) { removedId = this._remove(id[i]); if (removedId != null) { removedIds.push(removedId); } } } else { removedId = this._remove(id); if (removedId != null) { removedIds.push(removedId); } } if (removedIds.length) { this._trigger('remove', { items: removedIds }, senderId); } return removedIds; }; /** * Remove an item by its id * @param {Number | String | Object} id id or item * @returns {Number | String | null} id * @private */ DataSet.prototype._remove = function (id) { if (util.isNumber(id) || util.isString(id)) { if (this._data[id]) { delete this._data[id]; this.length--; return id; } } else if (id instanceof Object) { var itemId = id[this._fieldId]; if (itemId && this._data[itemId]) { delete this._data[itemId]; this.length--; return itemId; } } return null; }; /** * Clear the data * @param {String} [senderId] Optional sender id * @return {Array} removedIds The ids of all removed items */ DataSet.prototype.clear = function (senderId) { var ids = Object.keys(this._data); this._data = {}; this.length = 0; this._trigger('remove', { items: ids }, senderId); return ids; }; /** * Find the item with maximum value of a specified field * @param {String} field * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.max = function (field) { var data = this._data, max = null, maxField = null; for (var id in data) { if (data.hasOwnProperty(id)) { var item = data[id]; var itemField = item[field]; if (itemField != null && (!max || itemField > maxField)) { max = item; maxField = itemField; } } } return max; }; /** * Find the item with minimum value of a specified field * @param {String} field * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.min = function (field) { var data = this._data, min = null, minField = null; for (var id in data) { if (data.hasOwnProperty(id)) { var item = data[id]; var itemField = item[field]; if (itemField != null && (!min || itemField < minField)) { min = item; minField = itemField; } } } return min; }; /** * Find all distinct values of a specified field * @param {String} field * @return {Array} values Array containing all distinct values. If data items * do not contain the specified field are ignored. * The returned array is unordered. */ DataSet.prototype.distinct = function (field) { var data = this._data; var values = []; var fieldType = this._options.type && this._options.type[field] || null; var count = 0; var i; for (var prop in data) { if (data.hasOwnProperty(prop)) { var item = data[prop]; var value = item[field]; var exists = false; for (i = 0; i < count; i++) { if (values[i] == value) { exists = true; break; } } if (!exists && value !== undefined) { values[count] = value; count++; } } } if (fieldType) { for (i = 0; i < values.length; i++) { values[i] = util.convert(values[i], fieldType); } } return values; }; /** * Add a single item. Will fail when an item with the same id already exists. * @param {Object} item * @return {String} id * @private */ DataSet.prototype._addItem = function (item) { var id = item[this._fieldId]; if (id != undefined) { // check whether this id is already taken if (this._data[id]) { // item already exists throw new Error('Cannot add item: item with id ' + id + ' already exists'); } } else { // generate an id id = util.randomUUID(); item[this._fieldId] = id; } var d = {}; for (var field in item) { if (item.hasOwnProperty(field)) { var fieldType = this._type[field]; // type may be undefined d[field] = util.convert(item[field], fieldType); } } this._data[id] = d; this.length++; return id; }; /** * Get an item. Fields can be converted to a specific type * @param {String} id * @param {Object.} [types] field types to convert * @return {Object | null} item * @private */ DataSet.prototype._getItem = function (id, types) { var field, value; // get the item from the dataset var raw = this._data[id]; if (!raw) { return null; } // convert the items field types var converted = {}; if (types) { for (field in raw) { if (raw.hasOwnProperty(field)) { value = raw[field]; converted[field] = util.convert(value, types[field]); } } } else { // no field types specified, no converting needed for (field in raw) { if (raw.hasOwnProperty(field)) { value = raw[field]; converted[field] = value; } } } return converted; }; /** * Update a single item: merge with existing item. * Will fail when the item has no id, or when there does not exist an item * with the same id. * @param {Object} item * @return {String} id * @private */ DataSet.prototype._updateItem = function (item) { var id = item[this._fieldId]; if (id == undefined) { throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); } var d = this._data[id]; if (!d) { // item doesn't exist throw new Error('Cannot update item: no item with id ' + id + ' found'); } // merge with current item for (var field in item) { if (item.hasOwnProperty(field)) { var fieldType = this._type[field]; // type may be undefined d[field] = util.convert(item[field], fieldType); } } return id; }; module.exports = DataSet; /***/ }, /* 9 */ /***/ function(module, exports) { /** * A queue * @param {Object} options * Available options: * - delay: number When provided, the queue will be flushed * automatically after an inactivity of this delay * in milliseconds. * Default value is null. * - max: number When the queue exceeds the given maximum number * of entries, the queue is flushed automatically. * Default value of max is Infinity. * @constructor */ 'use strict'; function Queue(options) { // options this.delay = null; this.max = Infinity; // properties this._queue = []; this._timeout = null; this._extended = null; this.setOptions(options); } /** * Update the configuration of the queue * @param {Object} options * Available options: * - delay: number When provided, the queue will be flushed * automatically after an inactivity of this delay * in milliseconds. * Default value is null. * - max: number When the queue exceeds the given maximum number * of entries, the queue is flushed automatically. * Default value of max is Infinity. * @param options */ Queue.prototype.setOptions = function (options) { if (options && typeof options.delay !== 'undefined') { this.delay = options.delay; } if (options && typeof options.max !== 'undefined') { this.max = options.max; } this._flushIfNeeded(); }; /** * Extend an object with queuing functionality. * The object will be extended with a function flush, and the methods provided * in options.replace will be replaced with queued ones. * @param {Object} object * @param {Object} options * Available options: * - replace: Array. * A list with method names of the methods * on the object to be replaced with queued ones. * - delay: number When provided, the queue will be flushed * automatically after an inactivity of this delay * in milliseconds. * Default value is null. * - max: number When the queue exceeds the given maximum number * of entries, the queue is flushed automatically. * Default value of max is Infinity. * @return {Queue} Returns the created queue */ Queue.extend = function (object, options) { var queue = new Queue(options); if (object.flush !== undefined) { throw new Error('Target object already has a property flush'); } object.flush = function () { queue.flush(); }; var methods = [{ name: 'flush', original: undefined }]; if (options && options.replace) { for (var i = 0; i < options.replace.length; i++) { var name = options.replace[i]; methods.push({ name: name, original: object[name] }); queue.replace(object, name); } } queue._extended = { object: object, methods: methods }; return queue; }; /** * Destroy the queue. The queue will first flush all queued actions, and in * case it has extended an object, will restore the original object. */ Queue.prototype.destroy = function () { this.flush(); if (this._extended) { var object = this._extended.object; var methods = this._extended.methods; for (var i = 0; i < methods.length; i++) { var method = methods[i]; if (method.original) { object[method.name] = method.original; } else { delete object[method.name]; } } this._extended = null; } }; /** * Replace a method on an object with a queued version * @param {Object} object Object having the method * @param {string} method The method name */ Queue.prototype.replace = function (object, method) { var me = this; var original = object[method]; if (!original) { throw new Error('Method ' + method + ' undefined'); } object[method] = function () { // create an Array with the arguments var args = []; for (var i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } // add this call to the queue me.queue({ args: args, fn: original, context: this }); }; }; /** * Queue a call * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry */ Queue.prototype.queue = function (entry) { if (typeof entry === 'function') { this._queue.push({ fn: entry }); } else { this._queue.push(entry); } this._flushIfNeeded(); }; /** * Check whether the queue needs to be flushed * @private */ Queue.prototype._flushIfNeeded = function () { // flush when the maximum is exceeded. if (this._queue.length > this.max) { this.flush(); } // flush after a period of inactivity when a delay is configured clearTimeout(this._timeout); if (this.queue.length > 0 && typeof this.delay === 'number') { var me = this; this._timeout = setTimeout(function () { me.flush(); }, this.delay); } }; /** * Flush all queued calls */ Queue.prototype.flush = function () { while (this._queue.length > 0) { var entry = this._queue.shift(); entry.fn.apply(entry.context || entry.fn, entry.args || []); } }; module.exports = Queue; /***/ }, /* 10 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var DataSet = __webpack_require__(8); /** * DataView * * a dataview offers a filtered view on a dataset or an other dataview. * * @param {DataSet | DataView} data * @param {Object} [options] Available options: see method get * * @constructor DataView */ function DataView(data, options) { this._data = null; this._ids = {}; // ids of the items currently in memory (just contains a boolean true) this.length = 0; // number of items in the DataView this._options = options || {}; this._fieldId = 'id'; // name of the field containing id this._subscribers = {}; // event subscribers var me = this; this.listener = function () { me._onEvent.apply(me, arguments); }; this.setData(data); } // TODO: implement a function .config() to dynamically update things like configured filter // and trigger changes accordingly /** * Set a data source for the view * @param {DataSet | DataView} data */ DataView.prototype.setData = function (data) { var ids, i, len; if (this._data) { // unsubscribe from current dataset if (this._data.off) { this._data.off('*', this.listener); } // trigger a remove of all items in memory ids = []; for (var id in this._ids) { if (this._ids.hasOwnProperty(id)) { ids.push(id); } } this._ids = {}; this.length = 0; this._trigger('remove', { items: ids }); } this._data = data; if (this._data) { // update fieldId this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; // trigger an add of all added items ids = this._data.getIds({ filter: this._options && this._options.filter }); for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; this._ids[id] = true; } this.length = ids.length; this._trigger('add', { items: ids }); // subscribe to new dataset if (this._data.on) { this._data.on('*', this.listener); } } }; /** * Refresh the DataView. Useful when the DataView has a filter function * containing a variable parameter. */ DataView.prototype.refresh = function () { var id; var ids = this._data.getIds({ filter: this._options && this._options.filter }); var newIds = {}; var added = []; var removed = []; // check for additions for (var i = 0; i < ids.length; i++) { id = ids[i]; newIds[id] = true; if (!this._ids[id]) { added.push(id); this._ids[id] = true; this.length++; } } // check for removals for (id in this._ids) { if (this._ids.hasOwnProperty(id)) { if (!newIds[id]) { removed.push(id); delete this._ids[id]; this.length--; } } } // trigger events if (added.length) { this._trigger('add', { items: added }); } if (removed.length) { this._trigger('remove', { items: removed }); } }; /** * Get data from the data view * * Usage: * * get() * get(options: Object) * get(options: Object, data: Array | DataTable) * * get(id: Number) * get(id: Number, options: Object) * get(id: Number, options: Object, data: Array | DataTable) * * get(ids: Number[]) * get(ids: Number[], options: Object) * get(ids: Number[], options: Object, data: Array | DataTable) * * Where: * * {Number | String} id The id of an item * {Number[] | String{}} ids An array with ids of items * {Object} options An Object with options. Available options: * {String} [type] Type of data to be returned. Can * be 'DataTable' or 'Array' (default) * {Object.} [convert] * {String[]} [fields] field names to be returned * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * {Array | DataTable} [data] If provided, items will be appended to this * array or table. Required in case of Google * DataTable. * @param args */ DataView.prototype.get = function (args) { var me = this; // parse the arguments var ids, options, data; var firstType = util.getType(arguments[0]); if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { // get(id(s) [, options] [, data]) ids = arguments[0]; // can be a single id or an array with ids options = arguments[1]; data = arguments[2]; } else { // get([, options] [, data]) options = arguments[0]; data = arguments[1]; } // extend the options with the default options and provided options var viewOptions = util.extend({}, this._options, options); // create a combined filter method when needed if (this._options.filter && options && options.filter) { viewOptions.filter = function (item) { return me._options.filter(item) && options.filter(item); }; } // build up the call to the linked data set var getArguments = []; if (ids != undefined) { getArguments.push(ids); } getArguments.push(viewOptions); getArguments.push(data); return this._data && this._data.get.apply(this._data, getArguments); }; /** * Get ids of all items or from a filtered set of items. * @param {Object} [options] An Object with options. Available options: * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Array} ids */ DataView.prototype.getIds = function (options) { var ids; if (this._data) { var defaultFilter = this._options.filter; var filter; if (options && options.filter) { if (defaultFilter) { filter = function (item) { return defaultFilter(item) && options.filter(item); }; } else { filter = options.filter; } } else { filter = defaultFilter; } ids = this._data.getIds({ filter: filter, order: options && options.order }); } else { ids = []; } return ids; }; /** * Get the DataSet to which this DataView is connected. In case there is a chain * of multiple DataViews, the root DataSet of this chain is returned. * @return {DataSet} dataSet */ DataView.prototype.getDataSet = function () { var dataSet = this; while (dataSet instanceof DataView) { dataSet = dataSet._data; } return dataSet || null; }; /** * Event listener. Will propagate all events from the connected data set to * the subscribers of the DataView, but will filter the items and only trigger * when there are changes in the filtered data set. * @param {String} event * @param {Object | null} params * @param {String} senderId * @private */ DataView.prototype._onEvent = function (event, params, senderId) { var i, len, id, item; var ids = params && params.items; var data = this._data; var updatedData = []; var added = []; var updated = []; var removed = []; if (ids && data) { switch (event) { case 'add': // filter the ids of the added items for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; item = this.get(id); if (item) { this._ids[id] = true; added.push(id); } } break; case 'update': // determine the event from the views viewpoint: an updated // item can be added, updated, or removed from this view. for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; item = this.get(id); if (item) { if (this._ids[id]) { updated.push(id); updatedData.push(params.data[i]); } else { this._ids[id] = true; added.push(id); } } else { if (this._ids[id]) { delete this._ids[id]; removed.push(id); } else {} } } break; case 'remove': // filter the ids of the removed items for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; if (this._ids[id]) { delete this._ids[id]; removed.push(id); } } break; } this.length += added.length - removed.length; if (added.length) { this._trigger('add', { items: added }, senderId); } if (updated.length) { this._trigger('update', { items: updated, data: updatedData }, senderId); } if (removed.length) { this._trigger('remove', { items: removed }, senderId); } } }; // copy subscription functionality from DataSet DataView.prototype.on = DataSet.prototype.on; DataView.prototype.off = DataSet.prototype.off; DataView.prototype._trigger = DataSet.prototype._trigger; // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) DataView.prototype.subscribe = DataView.prototype.on; DataView.prototype.unsubscribe = DataView.prototype.off; module.exports = DataView; // nothing interesting for me :-( /***/ }, /* 11 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Emitter = __webpack_require__(13); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var util = __webpack_require__(1); var Point3d = __webpack_require__(14); var Point2d = __webpack_require__(12); var Camera = __webpack_require__(15); var Filter = __webpack_require__(16); var Slider = __webpack_require__(17); var StepNumber = __webpack_require__(18); /** * @constructor Graph3d * Graph3d displays data in 3d. * * Graph3d is developed in javascript as a Google Visualization Chart. * * @param {Element} container The DOM element in which the Graph3d will * be created. Normally a div element. * @param {DataSet | DataView | Array} [data] * @param {Object} [options] */ function Graph3d(container, data, options) { if (!(this instanceof Graph3d)) { throw new SyntaxError('Constructor must be called with the new operator'); } // create variables and set default values this.containerElement = container; this.width = '400px'; this.height = '400px'; this.margin = 10; // px this.defaultXCenter = '55%'; this.defaultYCenter = '50%'; this.xLabel = 'x'; this.yLabel = 'y'; this.zLabel = 'z'; var passValueFn = function passValueFn(v) { return v; }; this.xValueLabel = passValueFn; this.yValueLabel = passValueFn; this.zValueLabel = passValueFn; this.filterLabel = 'time'; this.legendLabel = 'value'; this.style = Graph3d.STYLE.DOT; this.showPerspective = true; this.showGrid = true; this.keepAspectRatio = true; this.showShadow = false; this.showGrayBottom = false; // TODO: this does not work correctly this.showTooltip = false; this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' this.animationInterval = 1000; // milliseconds this.animationPreload = false; this.camera = new Camera(); this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? this.dataTable = null; // The original data table this.dataPoints = null; // The table with point objects // the column indexes this.colX = undefined; this.colY = undefined; this.colZ = undefined; this.colValue = undefined; this.colFilter = undefined; this.xMin = 0; this.xStep = undefined; // auto by default this.xMax = 1; this.yMin = 0; this.yStep = undefined; // auto by default this.yMax = 1; this.zMin = 0; this.zStep = undefined; // auto by default this.zMax = 1; this.valueMin = 0; this.valueMax = 1; this.xBarWidth = 1; this.yBarWidth = 1; // TODO: customize axis range // constants this.colorAxis = '#4D4D4D'; this.colorGrid = '#D3D3D3'; this.colorDot = '#7DC1FF'; this.colorDotBorder = '#3267D2'; // create a frame and canvas this.create(); // apply options (also when undefined) this.setOptions(options); // apply data if (data) { this.setData(data); } } // Extend Graph3d with an Emitter mixin Emitter(Graph3d.prototype); /** * Calculate the scaling values, dependent on the range in x, y, and z direction */ Graph3d.prototype._setScale = function () { this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); // keep aspect ration between x and y scale if desired if (this.keepAspectRatio) { if (this.scale.x < this.scale.y) { //noinspection JSSuspiciousNameCombination this.scale.y = this.scale.x; } else { //noinspection JSSuspiciousNameCombination this.scale.x = this.scale.y; } } // scale the vertical axis this.scale.z *= this.verticalRatio; // TODO: can this be automated? verticalRatio? // determine scale for (optional) value this.scale.value = 1 / (this.valueMax - this.valueMin); // position the camera arm var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; this.camera.setArmLocation(xCenter, yCenter, zCenter); }; /** * Convert a 3D location to a 2D location on screen * http://en.wikipedia.org/wiki/3D_projection * @param {Point3d} point3d A 3D point with parameters x, y, z * @return {Point2d} point2d A 2D point with parameters x, y */ Graph3d.prototype._convert3Dto2D = function (point3d) { var translation = this._convertPointToTranslation(point3d); return this._convertTranslationToScreen(translation); }; /** * Convert a 3D location its translation seen from the camera * http://en.wikipedia.org/wiki/3D_projection * @param {Point3d} point3d A 3D point with parameters x, y, z * @return {Point3d} translation A 3D point with parameters x, y, z This is * the translation of the point, seen from the * camera */ Graph3d.prototype._convertPointToTranslation = function (point3d) { var ax = point3d.x * this.scale.x, ay = point3d.y * this.scale.y, az = point3d.z * this.scale.z, cx = this.camera.getCameraLocation().x, cy = this.camera.getCameraLocation().y, cz = this.camera.getCameraLocation().z, // calculate angles sinTx = Math.sin(this.camera.getCameraRotation().x), cosTx = Math.cos(this.camera.getCameraRotation().x), sinTy = Math.sin(this.camera.getCameraRotation().y), cosTy = Math.cos(this.camera.getCameraRotation().y), sinTz = Math.sin(this.camera.getCameraRotation().z), cosTz = Math.cos(this.camera.getCameraRotation().z), // calculate translation dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); return new Point3d(dx, dy, dz); }; /** * Convert a translation point to a point on the screen * @param {Point3d} translation A 3D point with parameters x, y, z This is * the translation of the point, seen from the * camera * @return {Point2d} point2d A 2D point with parameters x, y */ Graph3d.prototype._convertTranslationToScreen = function (translation) { var ex = this.eye.x, ey = this.eye.y, ez = this.eye.z, dx = translation.x, dy = translation.y, dz = translation.z; // calculate position on screen from translation var bx; var by; if (this.showPerspective) { bx = (dx - ex) * (ez / dz); by = (dy - ey) * (ez / dz); } else { bx = dx * -(ez / this.camera.getArmLength()); by = dy * -(ez / this.camera.getArmLength()); } // shift and scale the point to the center of the screen // use the width of the graph to scale both horizontally and vertically. return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); }; /** * Set the background styling for the graph * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ Graph3d.prototype._setBackgroundColor = function (backgroundColor) { var fill = 'white'; var stroke = 'gray'; var strokeWidth = 1; if (typeof backgroundColor === 'string') { fill = backgroundColor; stroke = 'none'; strokeWidth = 0; } else if (typeof backgroundColor === 'object') { if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; } else if (backgroundColor === undefined) {} else { throw 'Unsupported type of backgroundColor'; } this.frame.style.backgroundColor = fill; this.frame.style.borderColor = stroke; this.frame.style.borderWidth = strokeWidth + 'px'; this.frame.style.borderStyle = 'solid'; }; /// enumerate the available styles Graph3d.STYLE = { BAR: 0, BARCOLOR: 1, BARSIZE: 2, DOT: 3, DOTLINE: 4, DOTCOLOR: 5, DOTSIZE: 6, GRID: 7, LINE: 8, SURFACE: 9 }; /** * Retrieve the style index from given styleName * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' * @return {Number} styleNumber Enumeration value representing the style, or -1 * when not found */ Graph3d.prototype._getStyleNumber = function (styleName) { switch (styleName) { case 'dot': return Graph3d.STYLE.DOT; case 'dot-line': return Graph3d.STYLE.DOTLINE; case 'dot-color': return Graph3d.STYLE.DOTCOLOR; case 'dot-size': return Graph3d.STYLE.DOTSIZE; case 'line': return Graph3d.STYLE.LINE; case 'grid': return Graph3d.STYLE.GRID; case 'surface': return Graph3d.STYLE.SURFACE; case 'bar': return Graph3d.STYLE.BAR; case 'bar-color': return Graph3d.STYLE.BARCOLOR; case 'bar-size': return Graph3d.STYLE.BARSIZE; } return -1; }; /** * Determine the indexes of the data columns, based on the given style and data * @param {DataSet} data * @param {Number} style */ Graph3d.prototype._determineColumnIndexes = function (data, style) { if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { // 3 columns expected, and optionally a 4th with filter values this.colX = 0; this.colY = 1; this.colZ = 2; this.colValue = undefined; if (data.getNumberOfColumns() > 3) { this.colFilter = 3; } } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { // 4 columns expected, and optionally a 5th with filter values this.colX = 0; this.colY = 1; this.colZ = 2; this.colValue = 3; if (data.getNumberOfColumns() > 4) { this.colFilter = 4; } } else { throw 'Unknown style "' + this.style + '"'; } }; Graph3d.prototype.getNumberOfRows = function (data) { return data.length; }; Graph3d.prototype.getNumberOfColumns = function (data) { var counter = 0; for (var column in data[0]) { if (data[0].hasOwnProperty(column)) { counter++; } } return counter; }; Graph3d.prototype.getDistinctValues = function (data, column) { var distinctValues = []; for (var i = 0; i < data.length; i++) { if (distinctValues.indexOf(data[i][column]) == -1) { distinctValues.push(data[i][column]); } } return distinctValues; }; Graph3d.prototype.getColumnRange = function (data, column) { var minMax = { min: data[0][column], max: data[0][column] }; for (var i = 0; i < data.length; i++) { if (minMax.min > data[i][column]) { minMax.min = data[i][column]; } if (minMax.max < data[i][column]) { minMax.max = data[i][column]; } } return minMax; }; /** * Initialize the data from the data table. Calculate minimum and maximum values * and column index values * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. * @param {Number} style Style Number */ Graph3d.prototype._dataInitialize = function (rawData, style) { var me = this; // unsubscribe from the dataTable if (this.dataSet) { this.dataSet.off('*', this._onChange); } if (rawData === undefined) return; if (Array.isArray(rawData)) { rawData = new DataSet(rawData); } var data; if (rawData instanceof DataSet || rawData instanceof DataView) { data = rawData.get(); } else { throw new Error('Array, DataSet, or DataView expected'); } if (data.length == 0) return; this.dataSet = rawData; this.dataTable = data; // subscribe to changes in the dataset this._onChange = function () { me.setData(me.dataSet); }; this.dataSet.on('*', this._onChange); // _determineColumnIndexes // getNumberOfRows (points) // getNumberOfColumns (x,y,z,v,t,t1,t2...) // getDistinctValues (unique values?) // getColumnRange // determine the location of x,y,z,value,filter columns this.colX = 'x'; this.colY = 'y'; this.colZ = 'z'; this.colValue = 'style'; this.colFilter = 'filter'; // check if a filter column is provided if (data[0].hasOwnProperty('filter')) { if (this.dataFilter === undefined) { this.dataFilter = new Filter(rawData, this.colFilter, this); this.dataFilter.setOnLoadCallback(function () { me.redraw(); }); } } var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; // determine barWidth from data if (withBars) { if (this.defaultXBarWidth !== undefined) { this.xBarWidth = this.defaultXBarWidth; } else { var dataX = this.getDistinctValues(data, this.colX); this.xBarWidth = dataX[1] - dataX[0] || 1; } if (this.defaultYBarWidth !== undefined) { this.yBarWidth = this.defaultYBarWidth; } else { var dataY = this.getDistinctValues(data, this.colY); this.yBarWidth = dataY[1] - dataY[0] || 1; } } // calculate minimums and maximums var xRange = this.getColumnRange(data, this.colX); if (withBars) { xRange.min -= this.xBarWidth / 2; xRange.max += this.xBarWidth / 2; } this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; var yRange = this.getColumnRange(data, this.colY); if (withBars) { yRange.min -= this.yBarWidth / 2; yRange.max += this.yBarWidth / 2; } this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; var zRange = this.getColumnRange(data, this.colZ); this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; if (this.colValue !== undefined) { var valueRange = this.getColumnRange(data, this.colValue); this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; } // set the scale dependent on the ranges. this._setScale(); }; /** * Filter the data based on the current filter * @param {Array} data * @return {Array} dataPoints Array with point objects which can be drawn on screen */ Graph3d.prototype._getDataPoints = function (data) { // TODO: store the created matrix dataPoints in the filters instead of reloading each time var x, y, i, z, obj, point; var dataPoints = []; if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { // copy all values from the google data table to a matrix // the provided values are supposed to form a grid of (x,y) positions // create two lists with all present x and y values var dataX = []; var dataY = []; for (i = 0; i < this.getNumberOfRows(data); i++) { x = data[i][this.colX] || 0; y = data[i][this.colY] || 0; if (dataX.indexOf(x) === -1) { dataX.push(x); } if (dataY.indexOf(y) === -1) { dataY.push(y); } } var sortNumber = function sortNumber(a, b) { return a - b; }; dataX.sort(sortNumber); dataY.sort(sortNumber); // create a grid, a 2d matrix, with all values. var dataMatrix = []; // temporary data matrix for (i = 0; i < data.length; i++) { x = data[i][this.colX] || 0; y = data[i][this.colY] || 0; z = data[i][this.colZ] || 0; var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer var yIndex = dataY.indexOf(y); if (dataMatrix[xIndex] === undefined) { dataMatrix[xIndex] = []; } var point3d = new Point3d(); point3d.x = x; point3d.y = y; point3d.z = z; obj = {}; obj.point = point3d; obj.trans = undefined; obj.screen = undefined; obj.bottom = new Point3d(x, y, this.zMin); dataMatrix[xIndex][yIndex] = obj; dataPoints.push(obj); } // fill in the pointers to the neighbors. for (x = 0; x < dataMatrix.length; x++) { for (y = 0; y < dataMatrix[x].length; y++) { if (dataMatrix[x][y]) { dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined; dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined; dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined; } } } } else { // 'dot', 'dot-line', etc. // copy all values from the google data table to a list with Point3d objects for (i = 0; i < data.length; i++) { point = new Point3d(); point.x = data[i][this.colX] || 0; point.y = data[i][this.colY] || 0; point.z = data[i][this.colZ] || 0; if (this.colValue !== undefined) { point.value = data[i][this.colValue] || 0; } obj = {}; obj.point = point; obj.bottom = new Point3d(point.x, point.y, this.zMin); obj.trans = undefined; obj.screen = undefined; dataPoints.push(obj); } } return dataPoints; }; /** * Create the main frame for the Graph3d. * This function is executed once when a Graph3d object is created. The frame * contains a canvas, and this canvas contains all objects like the axis and * nodes. */ Graph3d.prototype.create = function () { // remove all elements from the container element. while (this.containerElement.hasChildNodes()) { this.containerElement.removeChild(this.containerElement.firstChild); } this.frame = document.createElement('div'); this.frame.style.position = 'relative'; this.frame.style.overflow = 'hidden'; // create the graph canvas (HTML canvas element) this.frame.canvas = document.createElement('canvas'); this.frame.canvas.style.position = 'relative'; this.frame.appendChild(this.frame.canvas); //if (!this.frame.canvas.getContext) { { var noCanvas = document.createElement('DIV'); noCanvas.style.color = 'red'; noCanvas.style.fontWeight = 'bold'; noCanvas.style.padding = '10px'; noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; this.frame.canvas.appendChild(noCanvas); } this.frame.filter = document.createElement('div'); this.frame.filter.style.position = 'absolute'; this.frame.filter.style.bottom = '0px'; this.frame.filter.style.left = '0px'; this.frame.filter.style.width = '100%'; this.frame.appendChild(this.frame.filter); // add event listeners to handle moving and zooming the contents var me = this; var onmousedown = function onmousedown(event) { me._onMouseDown(event); }; var ontouchstart = function ontouchstart(event) { me._onTouchStart(event); }; var onmousewheel = function onmousewheel(event) { me._onWheel(event); }; var ontooltip = function ontooltip(event) { me._onTooltip(event); }; // TODO: these events are never cleaned up... can give a 'memory leakage' util.addEventListener(this.frame.canvas, 'keydown', onkeydown); util.addEventListener(this.frame.canvas, 'mousedown', onmousedown); util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart); util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel); util.addEventListener(this.frame.canvas, 'mousemove', ontooltip); // add the new graph to the container element this.containerElement.appendChild(this.frame); }; /** * Set a new size for the graph * @param {string} width Width in pixels or percentage (for example '800px' * or '50%') * @param {string} height Height in pixels or percentage (for example '400px' * or '30%') */ Graph3d.prototype.setSize = function (width, height) { this.frame.style.width = width; this.frame.style.height = height; this._resizeCanvas(); }; /** * Resize the canvas to the current size of the frame */ Graph3d.prototype._resizeCanvas = function () { this.frame.canvas.style.width = '100%'; this.frame.canvas.style.height = '100%'; this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.height = this.frame.canvas.clientHeight; // adjust with for margin this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; }; /** * Start animation */ Graph3d.prototype.animationStart = function () { if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; this.frame.filter.slider.play(); }; /** * Stop animation */ Graph3d.prototype.animationStop = function () { if (!this.frame.filter || !this.frame.filter.slider) return; this.frame.filter.slider.stop(); }; /** * Resize the center position based on the current values in this.defaultXCenter * and this.defaultYCenter (which are strings with a percentage or a value * in pixels). The center positions are the variables this.xCenter * and this.yCenter */ Graph3d.prototype._resizeCenter = function () { // calculate the horizontal center position if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === '%') { this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; } else { this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px } // calculate the vertical center position if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === '%') { this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); } else { this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px } }; /** * Set the rotation and distance of the camera * @param {Object} pos An object with the camera position. The object * contains three parameters: * - horizontal {Number} * The horizontal rotation, between 0 and 2*PI. * Optional, can be left undefined. * - vertical {Number} * The vertical rotation, between 0 and 0.5*PI * if vertical=0.5*PI, the graph is shown from the * top. Optional, can be left undefined. * - distance {Number} * The (normalized) distance of the camera to the * center of the graph, a value between 0.71 and 5.0. * Optional, can be left undefined. */ Graph3d.prototype.setCameraPosition = function (pos) { if (pos === undefined) { return; } if (pos.horizontal !== undefined && pos.vertical !== undefined) { this.camera.setArmRotation(pos.horizontal, pos.vertical); } if (pos.distance !== undefined) { this.camera.setArmLength(pos.distance); } this.redraw(); }; /** * Retrieve the current camera rotation * @return {object} An object with parameters horizontal, vertical, and * distance */ Graph3d.prototype.getCameraPosition = function () { var pos = this.camera.getArmRotation(); pos.distance = this.camera.getArmLength(); return pos; }; /** * Load data into the 3D Graph */ Graph3d.prototype._readData = function (data) { // read the data this._dataInitialize(data, this.style); if (this.dataFilter) { // apply filtering this.dataPoints = this.dataFilter._getDataPoints(); } else { // no filtering. load all data this.dataPoints = this._getDataPoints(this.dataTable); } // draw the filter this._redrawFilter(); }; /** * Replace the dataset of the Graph3d * @param {Array | DataSet | DataView} data */ Graph3d.prototype.setData = function (data) { this._readData(data); this.redraw(); // start animation when option is true if (this.animationAutoStart && this.dataFilter) { this.animationStart(); } }; /** * Update the options. Options will be merged with current options * @param {Object} options */ Graph3d.prototype.setOptions = function (options) { var cameraPosition = undefined; this.animationStop(); if (options !== undefined) { // retrieve parameter values if (options.width !== undefined) this.width = options.width; if (options.height !== undefined) this.height = options.height; if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; if (options.xLabel !== undefined) this.xLabel = options.xLabel; if (options.yLabel !== undefined) this.yLabel = options.yLabel; if (options.zLabel !== undefined) this.zLabel = options.zLabel; if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; if (options.style !== undefined) { var styleNumber = this._getStyleNumber(options.style); if (styleNumber !== -1) { this.style = styleNumber; } } if (options.showGrid !== undefined) this.showGrid = options.showGrid; if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; if (options.showShadow !== undefined) this.showShadow = options.showShadow; if (options.tooltip !== undefined) this.showTooltip = options.tooltip; if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; if (options.xMin !== undefined) this.defaultXMin = options.xMin; if (options.xStep !== undefined) this.defaultXStep = options.xStep; if (options.xMax !== undefined) this.defaultXMax = options.xMax; if (options.yMin !== undefined) this.defaultYMin = options.yMin; if (options.yStep !== undefined) this.defaultYStep = options.yStep; if (options.yMax !== undefined) this.defaultYMax = options.yMax; if (options.zMin !== undefined) this.defaultZMin = options.zMin; if (options.zStep !== undefined) this.defaultZStep = options.zStep; if (options.zMax !== undefined) this.defaultZMax = options.zMax; if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; if (cameraPosition !== undefined) { this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); this.camera.setArmLength(cameraPosition.distance); } else { this.camera.setArmRotation(1, 0.5); this.camera.setArmLength(1.7); } } this._setBackgroundColor(options && options.backgroundColor); this.setSize(this.width, this.height); // re-load the data if (this.dataTable) { this.setData(this.dataTable); } // start animation when option is true if (this.animationAutoStart && this.dataFilter) { this.animationStart(); } }; /** * Redraw the Graph. */ Graph3d.prototype.redraw = function () { if (this.dataPoints === undefined) { throw 'Error: graph data not initialized'; } this._resizeCanvas(); this._resizeCenter(); this._redrawSlider(); this._redrawClear(); this._redrawAxis(); if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { this._redrawDataGrid(); } else if (this.style === Graph3d.STYLE.LINE) { this._redrawDataLine(); } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { this._redrawDataBar(); } else { // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE this._redrawDataDot(); } this._redrawInfo(); this._redrawLegend(); }; /** * Clear the canvas before redrawing */ Graph3d.prototype._redrawClear = function () { var canvas = this.frame.canvas; var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); }; /** * Redraw the legend showing the colors */ Graph3d.prototype._redrawLegend = function () { var y; if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { var dotSize = this.frame.clientWidth * 0.02; var widthMin, widthMax; if (this.style === Graph3d.STYLE.DOTSIZE) { widthMin = dotSize / 2; // px widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function } else { widthMin = 20; // px widthMax = 20; // px } var height = Math.max(this.frame.clientHeight * 0.25, 100); var top = this.margin; var right = this.frame.clientWidth - this.margin; var left = right - widthMax; var bottom = top + height; } var canvas = this.frame.canvas; var ctx = canvas.getContext('2d'); ctx.lineWidth = 1; ctx.font = '14px arial'; // TODO: put in options if (this.style === Graph3d.STYLE.DOTCOLOR) { // draw the color bar var ymin = 0; var ymax = height; // Todo: make height customizable for (y = ymin; y < ymax; y++) { var f = (y - ymin) / (ymax - ymin); //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function var hue = f * 240; var color = this._hsv2rgb(hue, 1, 1); ctx.strokeStyle = color; ctx.beginPath(); ctx.moveTo(left, top + y); ctx.lineTo(right, top + y); ctx.stroke(); } ctx.strokeStyle = this.colorAxis; ctx.strokeRect(left, top, widthMax, height); } if (this.style === Graph3d.STYLE.DOTSIZE) { // draw border around color bar ctx.strokeStyle = this.colorAxis; ctx.fillStyle = this.colorDot; ctx.beginPath(); ctx.moveTo(left, top); ctx.lineTo(right, top); ctx.lineTo(right - widthMax + widthMin, bottom); ctx.lineTo(left, bottom); ctx.closePath(); ctx.fill(); ctx.stroke(); } if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { // print values along the color bar var gridLineLen = 5; // px var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); step.start(); if (step.getCurrent() < this.valueMin) { step.next(); } while (!step.end()) { y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; ctx.beginPath(); ctx.moveTo(left - gridLineLen, y); ctx.lineTo(left, y); ctx.stroke(); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillStyle = this.colorAxis; ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); step.next(); } ctx.textAlign = 'right'; ctx.textBaseline = 'top'; var label = this.legendLabel; ctx.fillText(label, right, bottom + this.margin); } }; /** * Redraw the filter */ Graph3d.prototype._redrawFilter = function () { this.frame.filter.innerHTML = ''; if (this.dataFilter) { var options = { 'visible': this.showAnimationControls }; var slider = new Slider(this.frame.filter, options); this.frame.filter.slider = slider; // TODO: css here is not nice here... this.frame.filter.style.padding = '10px'; //this.frame.filter.style.backgroundColor = '#EFEFEF'; slider.setValues(this.dataFilter.values); slider.setPlayInterval(this.animationInterval); // create an event handler var me = this; var onchange = function onchange() { var index = slider.getIndex(); me.dataFilter.selectValue(index); me.dataPoints = me.dataFilter._getDataPoints(); me.redraw(); }; slider.setOnChangeCallback(onchange); } else { this.frame.filter.slider = undefined; } }; /** * Redraw the slider */ Graph3d.prototype._redrawSlider = function () { if (this.frame.filter.slider !== undefined) { this.frame.filter.slider.redraw(); } }; /** * Redraw common information */ Graph3d.prototype._redrawInfo = function () { if (this.dataFilter) { var canvas = this.frame.canvas; var ctx = canvas.getContext('2d'); ctx.font = '14px arial'; // TODO: put in options ctx.lineStyle = 'gray'; ctx.fillStyle = 'gray'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; var x = this.margin; var y = this.margin; ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); } }; /** * Redraw the axis */ Graph3d.prototype._redrawAxis = function () { var canvas = this.frame.canvas, ctx = canvas.getContext('2d'), from, to, step, prettyStep, text, xText, yText, zText, offset, xOffset, yOffset, xMin2d, xMax2d; // TODO: get the actual rendered style of the containerElement //ctx.font = this.containerElement.style.font; ctx.font = 24 / this.camera.getArmLength() + 'px arial'; // calculate the length for the short grid lines var gridLenX = 0.025 / this.scale.x; var gridLenY = 0.025 / this.scale.y; var textMargin = 5 / this.camera.getArmLength(); // px var armAngle = this.camera.getArmRotation().horizontal; // draw x-grid lines ctx.lineWidth = 1; prettyStep = this.defaultXStep === undefined; step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); step.start(); if (step.getCurrent() < this.xMin) { step.next(); } while (!step.end()) { var x = step.getCurrent(); if (this.showGrid) { from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); ctx.strokeStyle = this.colorGrid; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); } else { from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); } yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax; text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); if (Math.cos(armAngle * 2) > 0) { ctx.textAlign = 'center'; ctx.textBaseline = 'top'; text.y += textMargin; } else if (Math.sin(armAngle * 2) < 0) { ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; } else { ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; } ctx.fillStyle = this.colorAxis; ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); step.next(); } // draw y-grid lines ctx.lineWidth = 1; prettyStep = this.defaultYStep === undefined; step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); step.start(); if (step.getCurrent() < this.yMin) { step.next(); } while (!step.end()) { if (this.showGrid) { from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); ctx.strokeStyle = this.colorGrid; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); } else { from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); } xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax; text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); if (Math.cos(armAngle * 2) < 0) { ctx.textAlign = 'center'; ctx.textBaseline = 'top'; text.y += textMargin; } else if (Math.sin(armAngle * 2) > 0) { ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; } else { ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; } ctx.fillStyle = this.colorAxis; ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); step.next(); } // draw z-grid lines and axis ctx.lineWidth = 1; prettyStep = this.defaultZStep === undefined; step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); step.start(); if (step.getCurrent() < this.zMin) { step.next(); } xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; while (!step.end()) { // TODO: make z-grid lines really 3d? from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(from.x - textMargin, from.y); ctx.stroke(); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillStyle = this.colorAxis; ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); step.next(); } ctx.lineWidth = 1; from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); // draw x-axis ctx.lineWidth = 1; // line at yMin xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(xMin2d.x, xMin2d.y); ctx.lineTo(xMax2d.x, xMax2d.y); ctx.stroke(); // line at ymax xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(xMin2d.x, xMin2d.y); ctx.lineTo(xMax2d.x, xMax2d.y); ctx.stroke(); // draw y-axis ctx.lineWidth = 1; // line at xMin from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); // line at xMax from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); // draw x-label var xLabel = this.xLabel; if (xLabel.length > 0) { yOffset = 0.1 / this.scale.y; xText = (this.xMin + this.xMax) / 2; yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset; text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); if (Math.cos(armAngle * 2) > 0) { ctx.textAlign = 'center'; ctx.textBaseline = 'top'; } else if (Math.sin(armAngle * 2) < 0) { ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; } else { ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; } ctx.fillStyle = this.colorAxis; ctx.fillText(xLabel, text.x, text.y); } // draw y-label var yLabel = this.yLabel; if (yLabel.length > 0) { xOffset = 0.1 / this.scale.x; xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; yText = (this.yMin + this.yMax) / 2; text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); if (Math.cos(armAngle * 2) < 0) { ctx.textAlign = 'center'; ctx.textBaseline = 'top'; } else if (Math.sin(armAngle * 2) > 0) { ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; } else { ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; } ctx.fillStyle = this.colorAxis; ctx.fillText(yLabel, text.x, text.y); } // draw z-label var zLabel = this.zLabel; if (zLabel.length > 0) { offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; zText = (this.zMin + this.zMax) / 2; text = this._convert3Dto2D(new Point3d(xText, yText, zText)); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillStyle = this.colorAxis; ctx.fillText(zLabel, text.x - offset, text.y); } }; /** * Calculate the color based on the given value. * @param {Number} H Hue, a value be between 0 and 360 * @param {Number} S Saturation, a value between 0 and 1 * @param {Number} V Value, a value between 0 and 1 */ Graph3d.prototype._hsv2rgb = function (H, S, V) { var R, G, B, C, Hi, X; C = V * S; Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 X = C * (1 - Math.abs(H / 60 % 2 - 1)); switch (Hi) { case 0: R = C;G = X;B = 0;break; case 1: R = X;G = C;B = 0;break; case 2: R = 0;G = C;B = X;break; case 3: R = 0;G = X;B = C;break; case 4: R = X;G = 0;B = C;break; case 5: R = C;G = 0;B = X;break; default: R = 0;G = 0;B = 0;break; } return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; }; /** * Draw all datapoints as a grid * This function can be used when the style is 'grid' */ Graph3d.prototype._redrawDataGrid = function () { var canvas = this.frame.canvas, ctx = canvas.getContext('2d'), point, right, top, cross, i, topSideVisible, fillStyle, strokeStyle, lineWidth, h, s, v, zAvg; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations and screen position of all points for (i = 0; i < this.dataPoints.length; i++) { var trans = this._convertPointToTranslation(this.dataPoints[i].point); var screen = this._convertTranslationToScreen(trans); this.dataPoints[i].trans = trans; this.dataPoints[i].screen = screen; // calculate the translation of the point at the bottom (needed for sorting) var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } // sort the points on depth of their (x,y) position (not on z) var sortDepth = function sortDepth(a, b) { return b.dist - a.dist; }; this.dataPoints.sort(sortDepth); if (this.style === Graph3d.STYLE.SURFACE) { for (i = 0; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; right = this.dataPoints[i].pointRight; top = this.dataPoints[i].pointTop; cross = this.dataPoints[i].pointCross; if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { if (this.showGrayBottom || this.showShadow) { // calculate the cross product of the two vectors from center // to left and right, in order to know whether we are looking at the // bottom or at the top side. We can also use the cross product // for calculating light intensity var aDiff = Point3d.subtract(cross.trans, point.trans); var bDiff = Point3d.subtract(top.trans, right.trans); var crossproduct = Point3d.crossProduct(aDiff, bDiff); var len = crossproduct.length(); // FIXME: there is a bug with determining the surface side (shadow or colored) topSideVisible = crossproduct.z > 0; } else { topSideVisible = true; } if (topSideVisible) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; s = 1; // saturation if (this.showShadow) { v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale fillStyle = this._hsv2rgb(h, s, v); strokeStyle = fillStyle; } else { v = 1; fillStyle = this._hsv2rgb(h, s, v); strokeStyle = this.colorAxis; } } else { fillStyle = 'gray'; strokeStyle = this.colorAxis; } lineWidth = 0.5; ctx.lineWidth = lineWidth; ctx.fillStyle = fillStyle; ctx.strokeStyle = strokeStyle; ctx.beginPath(); ctx.moveTo(point.screen.x, point.screen.y); ctx.lineTo(right.screen.x, right.screen.y); ctx.lineTo(cross.screen.x, cross.screen.y); ctx.lineTo(top.screen.x, top.screen.y); ctx.closePath(); ctx.fill(); ctx.stroke(); } } } else { // grid style for (i = 0; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; right = this.dataPoints[i].pointRight; top = this.dataPoints[i].pointTop; if (point !== undefined) { if (this.showPerspective) { lineWidth = 2 / -point.trans.z; } else { lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); } } if (point !== undefined && right !== undefined) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 zAvg = (point.point.z + right.point.z) / 2; h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; ctx.lineWidth = lineWidth; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); ctx.beginPath(); ctx.moveTo(point.screen.x, point.screen.y); ctx.lineTo(right.screen.x, right.screen.y); ctx.stroke(); } if (point !== undefined && top !== undefined) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 zAvg = (point.point.z + top.point.z) / 2; h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; ctx.lineWidth = lineWidth; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); ctx.beginPath(); ctx.moveTo(point.screen.x, point.screen.y); ctx.lineTo(top.screen.x, top.screen.y); ctx.stroke(); } } } }; /** * Draw all datapoints as dots. * This function can be used when the style is 'dot' or 'dot-line' */ Graph3d.prototype._redrawDataDot = function () { var canvas = this.frame.canvas; var ctx = canvas.getContext('2d'); var i; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations of all points for (i = 0; i < this.dataPoints.length; i++) { var trans = this._convertPointToTranslation(this.dataPoints[i].point); var screen = this._convertTranslationToScreen(trans); this.dataPoints[i].trans = trans; this.dataPoints[i].screen = screen; // calculate the distance from the point at the bottom to the camera var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } // order the translated points by depth var sortDepth = function sortDepth(a, b) { return b.dist - a.dist; }; this.dataPoints.sort(sortDepth); // draw the datapoints as colored circles var dotSize = this.frame.clientWidth * 0.02; // px for (i = 0; i < this.dataPoints.length; i++) { var point = this.dataPoints[i]; if (this.style === Graph3d.STYLE.DOTLINE) { // draw a vertical line from the bottom to the graph value //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); var from = this._convert3Dto2D(point.bottom); ctx.lineWidth = 1; ctx.strokeStyle = this.colorGrid; ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(point.screen.x, point.screen.y); ctx.stroke(); } // calculate radius for the circle var size; if (this.style === Graph3d.STYLE.DOTSIZE) { size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); } else { size = dotSize; } var radius; if (this.showPerspective) { radius = size / -point.trans.z; } else { radius = size * -(this.eye.z / this.camera.getArmLength()); } if (radius < 0) { radius = 0; } var hue, color, borderColor; if (this.style === Graph3d.STYLE.DOTCOLOR) { // calculate the color based on the value hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); } else if (this.style === Graph3d.STYLE.DOTSIZE) { color = this.colorDot; borderColor = this.colorDotBorder; } else { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); } // draw the circle ctx.lineWidth = 1; ctx.strokeStyle = borderColor; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true); ctx.fill(); ctx.stroke(); } }; /** * Draw all datapoints as bars. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ Graph3d.prototype._redrawDataBar = function () { var canvas = this.frame.canvas; var ctx = canvas.getContext('2d'); var i, j, surface, corners; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations of all points for (i = 0; i < this.dataPoints.length; i++) { var trans = this._convertPointToTranslation(this.dataPoints[i].point); var screen = this._convertTranslationToScreen(trans); this.dataPoints[i].trans = trans; this.dataPoints[i].screen = screen; // calculate the distance from the point at the bottom to the camera var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } // order the translated points by depth var sortDepth = function sortDepth(a, b) { return b.dist - a.dist; }; this.dataPoints.sort(sortDepth); // draw the datapoints as bars var xWidth = this.xBarWidth / 2; var yWidth = this.yBarWidth / 2; for (i = 0; i < this.dataPoints.length; i++) { var point = this.dataPoints[i]; // determine color var hue, color, borderColor; if (this.style === Graph3d.STYLE.BARCOLOR) { // calculate the color based on the value hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); } else if (this.style === Graph3d.STYLE.BARSIZE) { color = this.colorDot; borderColor = this.colorDotBorder; } else { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); } // calculate size for the bar if (this.style === Graph3d.STYLE.BARSIZE) { xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); } // calculate all corner points var me = this; var point3d = point.point; var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }]; var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }]; // calculate screen location of the points top.forEach(function (obj) { obj.screen = me._convert3Dto2D(obj.point); }); bottom.forEach(function (obj) { obj.screen = me._convert3Dto2D(obj.point); }); // create five sides, calculate both corner points and center points var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }]; point.surfaces = surfaces; // calculate the distance of each of the surface centers to the camera for (j = 0; j < surfaces.length; j++) { surface = surfaces[j]; var transCenter = this._convertPointToTranslation(surface.center); surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; // TODO: this dept calculation doesn't work 100% of the cases due to perspective, // but the current solution is fast/simple and works in 99.9% of all cases // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) } // order the surfaces by their (translated) depth surfaces.sort(function (a, b) { var diff = b.dist - a.dist; if (diff) return diff; // if equal depth, sort the top surface last if (a.corners === top) return 1; if (b.corners === top) return -1; // both are equal return 0; }); // draw the ordered surfaces ctx.lineWidth = 1; ctx.strokeStyle = borderColor; ctx.fillStyle = color; // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside for (j = 2; j < surfaces.length; j++) { surface = surfaces[j]; corners = surface.corners; ctx.beginPath(); ctx.moveTo(corners[3].screen.x, corners[3].screen.y); ctx.lineTo(corners[0].screen.x, corners[0].screen.y); ctx.lineTo(corners[1].screen.x, corners[1].screen.y); ctx.lineTo(corners[2].screen.x, corners[2].screen.y); ctx.lineTo(corners[3].screen.x, corners[3].screen.y); ctx.fill(); ctx.stroke(); } } }; /** * Draw a line through all datapoints. * This function can be used when the style is 'line' */ Graph3d.prototype._redrawDataLine = function () { var canvas = this.frame.canvas, ctx = canvas.getContext('2d'), point, i; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations of all points for (i = 0; i < this.dataPoints.length; i++) { var trans = this._convertPointToTranslation(this.dataPoints[i].point); var screen = this._convertTranslationToScreen(trans); this.dataPoints[i].trans = trans; this.dataPoints[i].screen = screen; } // start the line if (this.dataPoints.length > 0) { point = this.dataPoints[0]; ctx.lineWidth = 1; // TODO: make customizable ctx.strokeStyle = 'blue'; // TODO: make customizable ctx.beginPath(); ctx.moveTo(point.screen.x, point.screen.y); } // draw the datapoints as colored circles for (i = 1; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; ctx.lineTo(point.screen.x, point.screen.y); } // finish the line if (this.dataPoints.length > 0) { ctx.stroke(); } }; /** * Start a moving operation inside the provided parent element * @param {Event} event The event that occurred (required for * retrieving the mouse position) */ Graph3d.prototype._onMouseDown = function (event) { event = event || window.event; // check if mouse is still down (may be up when focus is lost for example // in an iframe) if (this.leftButtonDown) { this._onMouseUp(event); } // only react on left mouse button down this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; if (!this.leftButtonDown && !this.touchDown) return; // get mouse position (different code for IE and all other browsers) this.startMouseX = getMouseX(event); this.startMouseY = getMouseY(event); this.startStart = new Date(this.start); this.startEnd = new Date(this.end); this.startArmRotation = this.camera.getArmRotation(); this.frame.style.cursor = 'move'; // add event listeners to handle moving the contents // we store the function onmousemove and onmouseup in the graph, so we can // remove the eventlisteners lateron in the function mouseUp() var me = this; this.onmousemove = function (event) { me._onMouseMove(event); }; this.onmouseup = function (event) { me._onMouseUp(event); }; util.addEventListener(document, 'mousemove', me.onmousemove); util.addEventListener(document, 'mouseup', me.onmouseup); util.preventDefault(event); }; /** * Perform moving operating. * This function activated from within the funcion Graph.mouseDown(). * @param {Event} event Well, eehh, the event */ Graph3d.prototype._onMouseMove = function (event) { event = event || window.event; // calculate change in mouse position var diffX = parseFloat(getMouseX(event)) - this.startMouseX; var diffY = parseFloat(getMouseY(event)) - this.startMouseY; var horizontalNew = this.startArmRotation.horizontal + diffX / 200; var verticalNew = this.startArmRotation.vertical + diffY / 200; var snapAngle = 4; // degrees var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... // the -0.001 is to take care that the vertical axis is always drawn at the left front corner if (Math.abs(Math.sin(horizontalNew)) < snapValue) { horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001; } if (Math.abs(Math.cos(horizontalNew)) < snapValue) { horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; } // snap vertically to nice angles if (Math.abs(Math.sin(verticalNew)) < snapValue) { verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; } if (Math.abs(Math.cos(verticalNew)) < snapValue) { verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; } this.camera.setArmRotation(horizontalNew, verticalNew); this.redraw(); // fire a cameraPositionChange event var parameters = this.getCameraPosition(); this.emit('cameraPositionChange', parameters); util.preventDefault(event); }; /** * Stop moving operating. * This function activated from within the funcion Graph.mouseDown(). * @param {event} event The event */ Graph3d.prototype._onMouseUp = function (event) { this.frame.style.cursor = 'auto'; this.leftButtonDown = false; // remove event listeners here util.removeEventListener(document, 'mousemove', this.onmousemove); util.removeEventListener(document, 'mouseup', this.onmouseup); util.preventDefault(event); }; /** * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point * @param {Event} event A mouse move event */ Graph3d.prototype._onTooltip = function (event) { var delay = 300; // ms var boundingRect = this.frame.getBoundingClientRect(); var mouseX = getMouseX(event) - boundingRect.left; var mouseY = getMouseY(event) - boundingRect.top; if (!this.showTooltip) { return; } if (this.tooltipTimeout) { clearTimeout(this.tooltipTimeout); } // (delayed) display of a tooltip only if no mouse button is down if (this.leftButtonDown) { this._hideTooltip(); return; } if (this.tooltip && this.tooltip.dataPoint) { // tooltip is currently visible var dataPoint = this._dataPointFromXY(mouseX, mouseY); if (dataPoint !== this.tooltip.dataPoint) { // datapoint changed if (dataPoint) { this._showTooltip(dataPoint); } else { this._hideTooltip(); } } } else { // tooltip is currently not visible var me = this; this.tooltipTimeout = setTimeout(function () { me.tooltipTimeout = null; // show a tooltip if we have a data point var dataPoint = me._dataPointFromXY(mouseX, mouseY); if (dataPoint) { me._showTooltip(dataPoint); } }, delay); } }; /** * Event handler for touchstart event on mobile devices */ Graph3d.prototype._onTouchStart = function (event) { this.touchDown = true; var me = this; this.ontouchmove = function (event) { me._onTouchMove(event); }; this.ontouchend = function (event) { me._onTouchEnd(event); }; util.addEventListener(document, 'touchmove', me.ontouchmove); util.addEventListener(document, 'touchend', me.ontouchend); this._onMouseDown(event); }; /** * Event handler for touchmove event on mobile devices */ Graph3d.prototype._onTouchMove = function (event) { this._onMouseMove(event); }; /** * Event handler for touchend event on mobile devices */ Graph3d.prototype._onTouchEnd = function (event) { this.touchDown = false; util.removeEventListener(document, 'touchmove', this.ontouchmove); util.removeEventListener(document, 'touchend', this.ontouchend); this._onMouseUp(event); }; /** * Event handler for mouse wheel event, used to zoom the graph * Code from http://adomas.org/javascript-mouse-wheel/ * @param {event} event The event */ Graph3d.prototype._onWheel = function (event) { if (!event) /* For IE. */ event = window.event; // retrieve delta var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ delta = event.wheelDelta / 120; } else if (event.detail) { /* Mozilla case. */ // In Mozilla, sign of delta is different than in IE. // Also, delta is multiple of 3. delta = -event.detail / 3; } // If delta is nonzero, handle it. // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. if (delta) { var oldLength = this.camera.getArmLength(); var newLength = oldLength * (1 - delta / 10); this.camera.setArmLength(newLength); this.redraw(); this._hideTooltip(); } // fire a cameraPositionChange event var parameters = this.getCameraPosition(); this.emit('cameraPositionChange', parameters); // Prevent default actions caused by mouse wheel. // That might be ugly, but we handle scrolls somehow // anyway, so don't bother here.. util.preventDefault(event); }; /** * Test whether a point lies inside given 2D triangle * @param {Point2d} point * @param {Point2d[]} triangle * @return {boolean} Returns true if given point lies inside or on the edge of the triangle * @private */ Graph3d.prototype._insideTriangle = function (point, triangle) { var a = triangle[0], b = triangle[1], c = triangle[2]; function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; } var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); // each of the three signs must be either equal to each other or zero return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs); }; /** * Find a data point close to given screen position (x, y) * @param {Number} x * @param {Number} y * @return {Object | null} The closest data point or null if not close to any data point * @private */ Graph3d.prototype._dataPointFromXY = function (x, y) { var i, distMax = 100, // px dataPoint = null, closestDataPoint = null, closestDist = null, center = new Point2d(x, y); if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { // the data points are ordered from far away to closest for (i = this.dataPoints.length - 1; i >= 0; i--) { dataPoint = this.dataPoints[i]; var surfaces = dataPoint.surfaces; if (surfaces) { for (var s = surfaces.length - 1; s >= 0; s--) { // split each surface in two triangles, and see if the center point is inside one of these var surface = surfaces[s]; var corners = surface.corners; var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) { // return immediately at the first hit return dataPoint; } } } } } else { // find the closest data point, using distance to the center of the point on 2d screen for (i = 0; i < this.dataPoints.length; i++) { dataPoint = this.dataPoints[i]; var point = dataPoint.screen; if (point) { var distX = Math.abs(x - point.x); var distY = Math.abs(y - point.y); var dist = Math.sqrt(distX * distX + distY * distY); if ((closestDist === null || dist < closestDist) && dist < distMax) { closestDist = dist; closestDataPoint = dataPoint; } } } } return closestDataPoint; }; /** * Display a tooltip for given data point * @param {Object} dataPoint * @private */ Graph3d.prototype._showTooltip = function (dataPoint) { var content, line, dot; if (!this.tooltip) { content = document.createElement('div'); content.style.position = 'absolute'; content.style.padding = '10px'; content.style.border = '1px solid #4d4d4d'; content.style.color = '#1a1a1a'; content.style.background = 'rgba(255,255,255,0.7)'; content.style.borderRadius = '2px'; content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; line = document.createElement('div'); line.style.position = 'absolute'; line.style.height = '40px'; line.style.width = '0'; line.style.borderLeft = '1px solid #4d4d4d'; dot = document.createElement('div'); dot.style.position = 'absolute'; dot.style.height = '0'; dot.style.width = '0'; dot.style.border = '5px solid #4d4d4d'; dot.style.borderRadius = '5px'; this.tooltip = { dataPoint: null, dom: { content: content, line: line, dot: dot } }; } else { content = this.tooltip.dom.content; line = this.tooltip.dom.line; dot = this.tooltip.dom.dot; } this._hideTooltip(); this.tooltip.dataPoint = dataPoint; if (typeof this.showTooltip === 'function') { content.innerHTML = this.showTooltip(dataPoint.point); } else { content.innerHTML = '' + '' + '' + '' + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; } content.style.left = '0'; content.style.top = '0'; this.frame.appendChild(content); this.frame.appendChild(line); this.frame.appendChild(dot); // calculate sizes var contentWidth = content.offsetWidth; var contentHeight = content.offsetHeight; var lineHeight = line.offsetHeight; var dotWidth = dot.offsetWidth; var dotHeight = dot.offsetHeight; var left = dataPoint.screen.x - contentWidth / 2; left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); line.style.left = dataPoint.screen.x + 'px'; line.style.top = dataPoint.screen.y - lineHeight + 'px'; content.style.left = left + 'px'; content.style.top = dataPoint.screen.y - lineHeight - contentHeight + 'px'; dot.style.left = dataPoint.screen.x - dotWidth / 2 + 'px'; dot.style.top = dataPoint.screen.y - dotHeight / 2 + 'px'; }; /** * Hide the tooltip when displayed * @private */ Graph3d.prototype._hideTooltip = function () { if (this.tooltip) { this.tooltip.dataPoint = null; for (var prop in this.tooltip.dom) { if (this.tooltip.dom.hasOwnProperty(prop)) { var elem = this.tooltip.dom[prop]; if (elem && elem.parentNode) { elem.parentNode.removeChild(elem); } } } } }; /**--------------------------------------------------------------------------**/ /** * Get the horizontal mouse position from a mouse event * @param {Event} event * @return {Number} mouse x */ function getMouseX(event) { if ('clientX' in event) return event.clientX; return event.targetTouches[0] && event.targetTouches[0].clientX || 0; } /** * Get the vertical mouse position from a mouse event * @param {Event} event * @return {Number} mouse y */ function getMouseY(event) { if ('clientY' in event) return event.clientY; return event.targetTouches[0] && event.targetTouches[0].clientY || 0; } module.exports = Graph3d; // use use defaults /***/ }, /* 12 */ /***/ function(module, exports) { /** * @prototype Point2d * @param {Number} [x] * @param {Number} [y] */ "use strict"; function Point2d(x, y) { this.x = x !== undefined ? x : 0; this.y = y !== undefined ? y : 0; } module.exports = Point2d; /***/ }, /* 13 */ /***/ function(module, exports) { /** * Expose `Emitter`. */ module.exports = Emitter; /** * Initialize a new `Emitter`. * * @api public */ function Emitter(obj) { if (obj) return mixin(obj); }; /** * Mixin the emitter properties. * * @param {Object} obj * @return {Object} * @api private */ function mixin(obj) { for (var key in Emitter.prototype) { obj[key] = Emitter.prototype[key]; } return obj; } /** * Listen on the given `event` with `fn`. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.on = Emitter.prototype.addEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; (this._callbacks[event] = this._callbacks[event] || []) .push(fn); return this; }; /** * Adds an `event` listener that will be invoked a single * time then automatically removed. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.once = function(event, fn){ var self = this; this._callbacks = this._callbacks || {}; function on() { self.off(event, on); fn.apply(this, arguments); } on.fn = fn; this.on(event, on); return this; }; /** * Remove the given callback for `event` or all * registered callbacks. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.off = Emitter.prototype.removeListener = Emitter.prototype.removeAllListeners = Emitter.prototype.removeEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; // all if (0 == arguments.length) { this._callbacks = {}; return this; } // specific event var callbacks = this._callbacks[event]; if (!callbacks) return this; // remove all handlers if (1 == arguments.length) { delete this._callbacks[event]; return this; } // remove specific handler var cb; for (var i = 0; i < callbacks.length; i++) { cb = callbacks[i]; if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1); break; } } return this; }; /** * Emit `event` with the given args. * * @param {String} event * @param {Mixed} ... * @return {Emitter} */ Emitter.prototype.emit = function(event){ this._callbacks = this._callbacks || {}; var args = [].slice.call(arguments, 1) , callbacks = this._callbacks[event]; if (callbacks) { callbacks = callbacks.slice(0); for (var i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(this, args); } } return this; }; /** * Return array of callbacks for `event`. * * @param {String} event * @return {Array} * @api public */ Emitter.prototype.listeners = function(event){ this._callbacks = this._callbacks || {}; return this._callbacks[event] || []; }; /** * Check if this emitter has `event` handlers. * * @param {String} event * @return {Boolean} * @api public */ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; /***/ }, /* 14 */ /***/ function(module, exports) { /** * @prototype Point3d * @param {Number} [x] * @param {Number} [y] * @param {Number} [z] */ "use strict"; function Point3d(x, y, z) { this.x = x !== undefined ? x : 0; this.y = y !== undefined ? y : 0; this.z = z !== undefined ? z : 0; }; /** * Subtract the two provided points, returns a-b * @param {Point3d} a * @param {Point3d} b * @return {Point3d} a-b */ Point3d.subtract = function (a, b) { var sub = new Point3d(); sub.x = a.x - b.x; sub.y = a.y - b.y; sub.z = a.z - b.z; return sub; }; /** * Add the two provided points, returns a+b * @param {Point3d} a * @param {Point3d} b * @return {Point3d} a+b */ Point3d.add = function (a, b) { var sum = new Point3d(); sum.x = a.x + b.x; sum.y = a.y + b.y; sum.z = a.z + b.z; return sum; }; /** * Calculate the average of two 3d points * @param {Point3d} a * @param {Point3d} b * @return {Point3d} The average, (a+b)/2 */ Point3d.avg = function (a, b) { return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); }; /** * Calculate the cross product of the two provided points, returns axb * Documentation: http://en.wikipedia.org/wiki/Cross_product * @param {Point3d} a * @param {Point3d} b * @return {Point3d} cross product axb */ Point3d.crossProduct = function (a, b) { var crossproduct = new Point3d(); crossproduct.x = a.y * b.z - a.z * b.y; crossproduct.y = a.z * b.x - a.x * b.z; crossproduct.z = a.x * b.y - a.y * b.x; return crossproduct; }; /** * Rtrieve the length of the vector (or the distance from this point to the origin * @return {Number} length */ Point3d.prototype.length = function () { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); }; module.exports = Point3d; /***/ }, /* 15 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Point3d = __webpack_require__(14); /** * @class Camera * The camera is mounted on a (virtual) camera arm. The camera arm can rotate * The camera is always looking in the direction of the origin of the arm. * This way, the camera always rotates around one fixed point, the location * of the camera arm. * * Documentation: * http://en.wikipedia.org/wiki/3D_projection */ function Camera() { this.armLocation = new Point3d(); this.armRotation = {}; this.armRotation.horizontal = 0; this.armRotation.vertical = 0; this.armLength = 1.7; this.cameraLocation = new Point3d(); this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); this.calculateCameraOrientation(); } /** * Set the location (origin) of the arm * @param {Number} x Normalized value of x * @param {Number} y Normalized value of y * @param {Number} z Normalized value of z */ Camera.prototype.setArmLocation = function (x, y, z) { this.armLocation.x = x; this.armLocation.y = y; this.armLocation.z = z; this.calculateCameraOrientation(); }; /** * Set the rotation of the camera arm * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. * Optional, can be left undefined. * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI * if vertical=0.5*PI, the graph is shown from the * top. Optional, can be left undefined. */ Camera.prototype.setArmRotation = function (horizontal, vertical) { if (horizontal !== undefined) { this.armRotation.horizontal = horizontal; } if (vertical !== undefined) { this.armRotation.vertical = vertical; if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI; } if (horizontal !== undefined || vertical !== undefined) { this.calculateCameraOrientation(); } }; /** * Retrieve the current arm rotation * @return {object} An object with parameters horizontal and vertical */ Camera.prototype.getArmRotation = function () { var rot = {}; rot.horizontal = this.armRotation.horizontal; rot.vertical = this.armRotation.vertical; return rot; }; /** * Set the (normalized) length of the camera arm. * @param {Number} length A length between 0.71 and 5.0 */ Camera.prototype.setArmLength = function (length) { if (length === undefined) return; this.armLength = length; // Radius must be larger than the corner of the graph, // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the // graph if (this.armLength < 0.71) this.armLength = 0.71; if (this.armLength > 5) this.armLength = 5; this.calculateCameraOrientation(); }; /** * Retrieve the arm length * @return {Number} length */ Camera.prototype.getArmLength = function () { return this.armLength; }; /** * Retrieve the camera location * @return {Point3d} cameraLocation */ Camera.prototype.getCameraLocation = function () { return this.cameraLocation; }; /** * Retrieve the camera rotation * @return {Point3d} cameraRotation */ Camera.prototype.getCameraRotation = function () { return this.cameraRotation; }; /** * Calculate the location and rotation of the camera based on the * position and orientation of the camera arm */ Camera.prototype.calculateCameraOrientation = function () { // calculate location of the camera this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); // calculate rotation of the camera this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; this.cameraRotation.y = 0; this.cameraRotation.z = -this.armRotation.horizontal; }; module.exports = Camera; /***/ }, /* 16 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var DataView = __webpack_require__(10); /** * @class Filter * * @param {DataSet} data The google data table * @param {Number} column The index of the column to be filtered * @param {Graph} graph The graph */ function Filter(data, column, graph) { this.data = data; this.column = column; this.graph = graph; // the parent graph this.index = undefined; this.value = undefined; // read all distinct values and select the first one this.values = graph.getDistinctValues(data.get(), this.column); // sort both numeric and string values correctly this.values.sort(function (a, b) { return a > b ? 1 : a < b ? -1 : 0; }); if (this.values.length > 0) { this.selectValue(0); } // create an array with the filtered datapoints. this will be loaded afterwards this.dataPoints = []; this.loaded = false; this.onLoadCallback = undefined; if (graph.animationPreload) { this.loaded = false; this.loadInBackground(); } else { this.loaded = true; } }; /** * Return the label * @return {string} label */ Filter.prototype.isLoaded = function () { return this.loaded; }; /** * Return the loaded progress * @return {Number} percentage between 0 and 100 */ Filter.prototype.getLoadedProgress = function () { var len = this.values.length; var i = 0; while (this.dataPoints[i]) { i++; } return Math.round(i / len * 100); }; /** * Return the label * @return {string} label */ Filter.prototype.getLabel = function () { return this.graph.filterLabel; }; /** * Return the columnIndex of the filter * @return {Number} columnIndex */ Filter.prototype.getColumn = function () { return this.column; }; /** * Return the currently selected value. Returns undefined if there is no selection * @return {*} value */ Filter.prototype.getSelectedValue = function () { if (this.index === undefined) return undefined; return this.values[this.index]; }; /** * Retrieve all values of the filter * @return {Array} values */ Filter.prototype.getValues = function () { return this.values; }; /** * Retrieve one value of the filter * @param {Number} index * @return {*} value */ Filter.prototype.getValue = function (index) { if (index >= this.values.length) throw 'Error: index out of range'; return this.values[index]; }; /** * Retrieve the (filtered) dataPoints for the currently selected filter index * @param {Number} [index] (optional) * @return {Array} dataPoints */ Filter.prototype._getDataPoints = function (index) { if (index === undefined) index = this.index; if (index === undefined) return []; var dataPoints; if (this.dataPoints[index]) { dataPoints = this.dataPoints[index]; } else { var f = {}; f.column = this.column; f.value = this.values[index]; var dataView = new DataView(this.data, { filter: function filter(item) { return item[f.column] == f.value; } }).get(); dataPoints = this.graph._getDataPoints(dataView); this.dataPoints[index] = dataPoints; } return dataPoints; }; /** * Set a callback function when the filter is fully loaded. */ Filter.prototype.setOnLoadCallback = function (callback) { this.onLoadCallback = callback; }; /** * Add a value to the list with available values for this filter * No double entries will be created. * @param {Number} index */ Filter.prototype.selectValue = function (index) { if (index >= this.values.length) throw 'Error: index out of range'; this.index = index; this.value = this.values[index]; }; /** * Load all filtered rows in the background one by one * Start this method without providing an index! */ Filter.prototype.loadInBackground = function (index) { if (index === undefined) index = 0; var frame = this.graph.frame; if (index < this.values.length) { var dataPointsTemp = this._getDataPoints(index); //this.graph.redrawInfo(); // TODO: not neat // create a progress box if (frame.progress === undefined) { frame.progress = document.createElement('DIV'); frame.progress.style.position = 'absolute'; frame.progress.style.color = 'gray'; frame.appendChild(frame.progress); } var progress = this.getLoadedProgress(); frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; // TODO: this is no nice solution... frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider frame.progress.style.left = 10 + 'px'; var me = this; setTimeout(function () { me.loadInBackground(index + 1); }, 10); this.loaded = false; } else { this.loaded = true; // remove the progress box if (frame.progress !== undefined) { frame.removeChild(frame.progress); frame.progress = undefined; } if (this.onLoadCallback) this.onLoadCallback(); } }; module.exports = Filter; /***/ }, /* 17 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); /** * @constructor Slider * * An html slider control with start/stop/prev/next buttons * @param {Element} container The element where the slider will be created * @param {Object} options Available options: * {boolean} visible If true (default) the * slider is visible. */ function Slider(container, options) { if (container === undefined) { throw 'Error: No container element defined'; } this.container = container; this.visible = options && options.visible != undefined ? options.visible : true; if (this.visible) { this.frame = document.createElement('DIV'); //this.frame.style.backgroundColor = '#E5E5E5'; this.frame.style.width = '100%'; this.frame.style.position = 'relative'; this.container.appendChild(this.frame); this.frame.prev = document.createElement('INPUT'); this.frame.prev.type = 'BUTTON'; this.frame.prev.value = 'Prev'; this.frame.appendChild(this.frame.prev); this.frame.play = document.createElement('INPUT'); this.frame.play.type = 'BUTTON'; this.frame.play.value = 'Play'; this.frame.appendChild(this.frame.play); this.frame.next = document.createElement('INPUT'); this.frame.next.type = 'BUTTON'; this.frame.next.value = 'Next'; this.frame.appendChild(this.frame.next); this.frame.bar = document.createElement('INPUT'); this.frame.bar.type = 'BUTTON'; this.frame.bar.style.position = 'absolute'; this.frame.bar.style.border = '1px solid red'; this.frame.bar.style.width = '100px'; this.frame.bar.style.height = '6px'; this.frame.bar.style.borderRadius = '2px'; this.frame.bar.style.MozBorderRadius = '2px'; this.frame.bar.style.border = '1px solid #7F7F7F'; this.frame.bar.style.backgroundColor = '#E5E5E5'; this.frame.appendChild(this.frame.bar); this.frame.slide = document.createElement('INPUT'); this.frame.slide.type = 'BUTTON'; this.frame.slide.style.margin = '0px'; this.frame.slide.value = ' '; this.frame.slide.style.position = 'relative'; this.frame.slide.style.left = '-100px'; this.frame.appendChild(this.frame.slide); // create events var me = this; this.frame.slide.onmousedown = function (event) { me._onMouseDown(event); }; this.frame.prev.onclick = function (event) { me.prev(event); }; this.frame.play.onclick = function (event) { me.togglePlay(event); }; this.frame.next.onclick = function (event) { me.next(event); }; } this.onChangeCallback = undefined; this.values = []; this.index = undefined; this.playTimeout = undefined; this.playInterval = 1000; // milliseconds this.playLoop = true; } /** * Select the previous index */ Slider.prototype.prev = function () { var index = this.getIndex(); if (index > 0) { index--; this.setIndex(index); } }; /** * Select the next index */ Slider.prototype.next = function () { var index = this.getIndex(); if (index < this.values.length - 1) { index++; this.setIndex(index); } }; /** * Select the next index */ Slider.prototype.playNext = function () { var start = new Date(); var index = this.getIndex(); if (index < this.values.length - 1) { index++; this.setIndex(index); } else if (this.playLoop) { // jump to the start index = 0; this.setIndex(index); } var end = new Date(); var diff = end - start; // calculate how much time it to to set the index and to execute the callback // function. var interval = Math.max(this.playInterval - diff, 0); // document.title = diff // TODO: cleanup var me = this; this.playTimeout = setTimeout(function () { me.playNext(); }, interval); }; /** * Toggle start or stop playing */ Slider.prototype.togglePlay = function () { if (this.playTimeout === undefined) { this.play(); } else { this.stop(); } }; /** * Start playing */ Slider.prototype.play = function () { // Test whether already playing if (this.playTimeout) return; this.playNext(); if (this.frame) { this.frame.play.value = 'Stop'; } }; /** * Stop playing */ Slider.prototype.stop = function () { clearInterval(this.playTimeout); this.playTimeout = undefined; if (this.frame) { this.frame.play.value = 'Play'; } }; /** * Set a callback function which will be triggered when the value of the * slider bar has changed. */ Slider.prototype.setOnChangeCallback = function (callback) { this.onChangeCallback = callback; }; /** * Set the interval for playing the list * @param {Number} interval The interval in milliseconds */ Slider.prototype.setPlayInterval = function (interval) { this.playInterval = interval; }; /** * Retrieve the current play interval * @return {Number} interval The interval in milliseconds */ Slider.prototype.getPlayInterval = function (interval) { return this.playInterval; }; /** * Set looping on or off * @pararm {boolean} doLoop If true, the slider will jump to the start when * the end is passed, and will jump to the end * when the start is passed. */ Slider.prototype.setPlayLoop = function (doLoop) { this.playLoop = doLoop; }; /** * Execute the onchange callback function */ Slider.prototype.onChange = function () { if (this.onChangeCallback !== undefined) { this.onChangeCallback(); } }; /** * redraw the slider on the correct place */ Slider.prototype.redraw = function () { if (this.frame) { // resize the bar this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px'; this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px'; // position the slider button var left = this.indexToLeft(this.index); this.frame.slide.style.left = left + 'px'; } }; /** * Set the list with values for the slider * @param {Array} values A javascript array with values (any type) */ Slider.prototype.setValues = function (values) { this.values = values; if (this.values.length > 0) this.setIndex(0);else this.index = undefined; }; /** * Select a value by its index * @param {Number} index */ Slider.prototype.setIndex = function (index) { if (index < this.values.length) { this.index = index; this.redraw(); this.onChange(); } else { throw 'Error: index out of range'; } }; /** * retrieve the index of the currently selected vaue * @return {Number} index */ Slider.prototype.getIndex = function () { return this.index; }; /** * retrieve the currently selected value * @return {*} value */ Slider.prototype.get = function () { return this.values[this.index]; }; Slider.prototype._onMouseDown = function (event) { // only react on left mouse button down var leftButtonDown = event.which ? event.which === 1 : event.button === 1; if (!leftButtonDown) return; this.startClientX = event.clientX; this.startSlideX = parseFloat(this.frame.slide.style.left); this.frame.style.cursor = 'move'; // add event listeners to handle moving the contents // we store the function onmousemove and onmouseup in the graph, so we can // remove the eventlisteners lateron in the function mouseUp() var me = this; this.onmousemove = function (event) { me._onMouseMove(event); }; this.onmouseup = function (event) { me._onMouseUp(event); }; util.addEventListener(document, 'mousemove', this.onmousemove); util.addEventListener(document, 'mouseup', this.onmouseup); util.preventDefault(event); }; Slider.prototype.leftToIndex = function (left) { var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; var x = left - 3; var index = Math.round(x / width * (this.values.length - 1)); if (index < 0) index = 0; if (index > this.values.length - 1) index = this.values.length - 1; return index; }; Slider.prototype.indexToLeft = function (index) { var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; var x = index / (this.values.length - 1) * width; var left = x + 3; return left; }; Slider.prototype._onMouseMove = function (event) { var diff = event.clientX - this.startClientX; var x = this.startSlideX + diff; var index = this.leftToIndex(x); this.setIndex(index); util.preventDefault(); }; Slider.prototype._onMouseUp = function (event) { this.frame.style.cursor = 'auto'; // remove event listeners util.removeEventListener(document, 'mousemove', this.onmousemove); util.removeEventListener(document, 'mouseup', this.onmouseup); util.preventDefault(); }; module.exports = Slider; /***/ }, /* 18 */ /***/ function(module, exports) { /** * @prototype StepNumber * The class StepNumber is an iterator for Numbers. You provide a start and end * value, and a best step size. StepNumber itself rounds to fixed values and * a finds the step that best fits the provided step. * * If prettyStep is true, the step size is chosen as close as possible to the * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... * * Example usage: * var step = new StepNumber(0, 10, 2.5, true); * step.start(); * while (!step.end()) { * alert(step.getCurrent()); * step.next(); * } * * Version: 1.0 * * @param {Number} start The start value * @param {Number} end The end value * @param {Number} step Optional. Step size. Must be a positive value. * @param {boolean} prettyStep Optional. If true, the step size is rounded * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ "use strict"; function StepNumber(start, end, step, prettyStep) { // set default values this._start = 0; this._end = 0; this._step = 1; this.prettyStep = true; this.precision = 5; this._current = 0; this.setRange(start, end, step, prettyStep); }; /** * Set a new range: start, end and step. * * @param {Number} start The start value * @param {Number} end The end value * @param {Number} step Optional. Step size. Must be a positive value. * @param {boolean} prettyStep Optional. If true, the step size is rounded * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ StepNumber.prototype.setRange = function (start, end, step, prettyStep) { this._start = start ? start : 0; this._end = end ? end : 0; this.setStep(step, prettyStep); }; /** * Set a new step size * @param {Number} step New step size. Must be a positive value * @param {boolean} prettyStep Optional. If true, the provided step is rounded * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ StepNumber.prototype.setStep = function (step, prettyStep) { if (step === undefined || step <= 0) return; if (prettyStep !== undefined) this.prettyStep = prettyStep; if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** * Calculate a nice step size, closest to the desired step size. * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an * integer Number. For example 1, 2, 5, 10, 20, 50, etc... * @param {Number} step Desired step size * @return {Number} Nice step size */ StepNumber.calculatePrettyStep = function (step) { var log10 = function log10(x) { return Math.log(x) / Math.LN10; }; // try three steps (multiple of 1, 2, or 5 var step1 = Math.pow(10, Math.round(log10(step))), step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); // choose the best step (closest to minimum step) var prettyStep = step1; if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; // for safety if (prettyStep <= 0) { prettyStep = 1; } return prettyStep; }; /** * returns the current value of the step * @return {Number} current value */ StepNumber.prototype.getCurrent = function () { return parseFloat(this._current.toPrecision(this.precision)); }; /** * returns the current step size * @return {Number} current step size */ StepNumber.prototype.getStep = function () { return this._step; }; /** * Set the current value to the largest value smaller than start, which * is a multiple of the step size */ StepNumber.prototype.start = function () { this._current = this._start - this._start % this._step; }; /** * Do a step, add the step size to the current value */ StepNumber.prototype.next = function () { this._current += this._step; }; /** * Returns true whether the end is reached * @return {boolean} True if the current value has passed the end value. */ StepNumber.prototype.end = function () { return this._current > this._end; }; module.exports = StepNumber; /***/ }, /* 19 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Emitter = __webpack_require__(13); var Hammer = __webpack_require__(23); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var Range = __webpack_require__(27); var Core = __webpack_require__(30); var TimeAxis = __webpack_require__(41); var CurrentTime = __webpack_require__(20); var CustomTime = __webpack_require__(44); var ItemSet = __webpack_require__(31); var Configurator = __webpack_require__(45); var Validator = __webpack_require__(47)['default']; var printStyle = __webpack_require__(47).printStyle; var allOptions = __webpack_require__(48).allOptions; var configureOptions = __webpack_require__(48).configureOptions; /** * Create a timeline visualization * @param {HTMLElement} container * @param {vis.DataSet | vis.DataView | Array} [items] * @param {vis.DataSet | vis.DataView | Array} [groups] * @param {Object} [options] See Timeline.setOptions for the available options. * @constructor * @extends Core */ function Timeline(container, items, groups, options) { if (!(this instanceof Timeline)) { throw new SyntaxError('Constructor must be called with the new operator'); } // if the third element is options, the forth is groups (optionally); if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { var forthArgument = options; options = groups; groups = forthArgument; } var me = this; this.defaultOptions = { start: null, end: null, autoResize: true, orientation: { axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' item: 'bottom' // not relevant }, width: null, height: null, maxHeight: null, minHeight: null }; this.options = util.deepExtend({}, this.defaultOptions); // Create the DOM, props, and emitter this._create(container); // all components listed here will be repainted automatically this.components = []; this.body = { dom: this.dom, domProps: this.props, emitter: { on: this.on.bind(this), off: this.off.bind(this), emit: this.emit.bind(this) }, hiddenDates: [], util: { getScale: function getScale() { return me.timeAxis.step.scale; }, getStep: function getStep() { return me.timeAxis.step.step; }, toScreen: me._toScreen.bind(me), toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toTime: me._toTime.bind(me), toGlobalTime: me._toGlobalTime.bind(me) } }; // range this.range = new Range(this.body); this.components.push(this.range); this.body.range = this.range; // time axis this.timeAxis = new TimeAxis(this.body); this.timeAxis2 = null; // used in case of orientation option 'both' this.components.push(this.timeAxis); // current time bar this.currentTime = new CurrentTime(this.body); this.components.push(this.currentTime); // item set this.itemSet = new ItemSet(this.body); this.components.push(this.itemSet); this.itemsData = null; // DataSet this.groupsData = null; // DataSet this.on('tap', function (event) { me.emit('click', me.getEventProperties(event)); }); this.on('doubletap', function (event) { me.emit('doubleClick', me.getEventProperties(event)); }); this.dom.root.oncontextmenu = function (event) { me.emit('contextmenu', me.getEventProperties(event)); }; // apply options if (options) { this.setOptions(options); } // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! if (groups) { this.setGroups(groups); } // create itemset if (items) { this.setItems(items); } else { this._redraw(); } } // Extend the functionality from Core Timeline.prototype = new Core(); /** * Load a configurator * @return {Object} * @private */ Timeline.prototype._createConfigurator = function () { return new Configurator(this, this.dom.container, configureOptions); }; /** * Force a redraw. The size of all items will be recalculated. * Can be useful to manually redraw when option autoResize=false and the window * has been resized, or when the items CSS has been changed. */ Timeline.prototype.redraw = function () { this.itemSet && this.itemSet.markDirty({ refreshItems: true }); this._redraw(); }; Timeline.prototype.setOptions = function (options) { // validate options var errorFound = Validator.validate(options, allOptions); if (errorFound === true) { console.log('%cErrors have been found in the supplied options object.', printStyle); } Core.prototype.setOptions.call(this, options); if ('type' in options) { if (options.type !== this.options.type) { this.options.type = options.type; // force recreation of all items var itemsData = this.itemsData; if (itemsData) { var selection = this.getSelection(); this.setItems(null); // remove all this.setItems(itemsData); // add all this.setSelection(selection); // restore selection } } } }; /** * Set items * @param {vis.DataSet | Array | null} items */ Timeline.prototype.setItems = function (items) { var initialLoad = this.itemsData == null; // convert to type DataSet when needed var newDataSet; if (!items) { newDataSet = null; } else if (items instanceof DataSet || items instanceof DataView) { newDataSet = items; } else { // turn an array into a dataset newDataSet = new DataSet(items, { type: { start: 'Date', end: 'Date' } }); } // set items this.itemsData = newDataSet; this.itemSet && this.itemSet.setItems(newDataSet); if (initialLoad) { if (this.options.start != undefined || this.options.end != undefined) { if (this.options.start == undefined || this.options.end == undefined) { var range = this.getItemRange(); } var start = this.options.start != undefined ? this.options.start : range.min; var end = this.options.end != undefined ? this.options.end : range.max; this.setWindow(start, end, { animation: false }); } else { this.fit({ animation: false }); } } }; /** * Set groups * @param {vis.DataSet | Array} groups */ Timeline.prototype.setGroups = function (groups) { // convert to type DataSet when needed var newDataSet; if (!groups) { newDataSet = null; } else if (groups instanceof DataSet || groups instanceof DataView) { newDataSet = groups; } else { // turn an array into a dataset newDataSet = new DataSet(groups); } this.groupsData = newDataSet; this.itemSet.setGroups(newDataSet); }; /** * Set both items and groups in one go * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data */ Timeline.prototype.setData = function (data) { if (data && data.groups) { this.setGroups(data.groups); } if (data && data.items) { this.setItems(data.items); } }; /** * Set selected items by their id. Replaces the current selection * Unknown id's are silently ignored. * @param {string[] | string} [ids] An array with zero or more id's of the items to be * selected. If ids is an empty array, all items will be * unselected. * @param {Object} [options] Available options: * `focus: boolean` * If true, focus will be set to the selected item(s) * `animation: boolean | {duration: number, easingFunction: string}` * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. * Only applicable when option focus is true. */ Timeline.prototype.setSelection = function (ids, options) { this.itemSet && this.itemSet.setSelection(ids); if (options && options.focus) { this.focus(ids, options); } }; /** * Get the selected items by their id * @return {Array} ids The ids of the selected items */ Timeline.prototype.getSelection = function () { return this.itemSet && this.itemSet.getSelection() || []; }; /** * Adjust the visible window such that the selected item (or multiple items) * are centered on screen. * @param {String | String[]} id An item id or array with item ids * @param {Object} [options] Available options: * `animation: boolean | {duration: number, easingFunction: string}` * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. */ Timeline.prototype.focus = function (id, options) { if (!this.itemsData || id == undefined) return; var ids = Array.isArray(id) ? id : [id]; // get the specified item(s) var itemsData = this.itemsData.getDataSet().get(ids, { type: { start: 'Date', end: 'Date' } }); // calculate minimum start and maximum end of specified items var start = null; var end = null; itemsData.forEach(function (itemData) { var s = itemData.start.valueOf(); var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); if (start === null || s < start) { start = s; } if (end === null || e > end) { end = e; } }); if (start !== null && end !== null) { // calculate the new middle and interval for the window var middle = (start + end) / 2; var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); var animation = options && options.animation !== undefined ? options.animation : true; this.range.setRange(middle - interval / 2, middle + interval / 2, animation); } }; /** * Set Timeline window such that it fits all items * @param {Object} [options] Available options: * `animation: boolean | {duration: number, easingFunction: string}` * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. */ Timeline.prototype.fit = function (options) { var animation = options && options.animation !== undefined ? options.animation : true; var range = this.getItemRange(); this.range.setRange(range.min, range.max, animation); }; /** * Determine the range of the items, taking into account their actual width * and a margin of 10 pixels on both sides. * @return {{min: Date | null, max: Date | null}} */ Timeline.prototype.getItemRange = function () { var _this = this; // get a rough approximation for the range based on the items start and end dates var range = this.getDataRange(); var min = range.min; var max = range.max; var minItem = null; var maxItem = null; if (min != null && max != null) { var interval; var factor; var lhs; var rhs; var delta; (function () { var getStart = function (item) { return util.convert(item.data.start, 'Date').valueOf(); }; var getEnd = function (item) { var end = item.data.end != undefined ? item.data.end : item.data.start; return util.convert(end, 'Date').valueOf(); }; interval = max - min; // ms if (interval <= 0) { interval = 10; } factor = interval / _this.props.center.width; // calculate the date of the left side and right side of the items given util.forEach(_this.itemSet.items, (function (item) { item.show(); var start = getStart(item); var end = getEnd(item); var left = new Date(start - (item.getWidthLeft() + 10) * factor); var right = new Date(end + (item.getWidthRight() + 10) * factor); if (left < min) { min = left; minItem = item; } if (right > max) { max = right; maxItem = item; } }).bind(_this)); if (minItem && maxItem) { lhs = minItem.getWidthLeft() + 10; rhs = maxItem.getWidthRight() + 10; delta = _this.props.center.width - lhs - rhs; // px if (delta > 0) { min = getStart(minItem) - lhs * interval / delta; // ms max = getEnd(maxItem) + rhs * interval / delta; // ms } } })(); } return { min: min != null ? new Date(min) : null, max: max != null ? new Date(max) : null }; }; /** * Calculate the data range of the items start and end dates * @returns {{min: Date | null, max: Date | null}} */ Timeline.prototype.getDataRange = function () { var min = null; var max = null; var dataset = this.itemsData && this.itemsData.getDataSet(); if (dataset) { dataset.forEach(function (item) { var start = util.convert(item.start, 'Date').valueOf(); var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf(); if (min === null || start < min) { min = start; } if (max === null || end > max) { max = start; } }); } return { min: min != null ? new Date(min) : null, max: max != null ? new Date(max) : null }; }; /** * Generate Timeline related information from an event * @param {Event} event * @return {Object} An object with related information, like on which area * The event happened, whether clicked on an item, etc. */ Timeline.prototype.getEventProperties = function (event) { var clientX = event.center ? event.center.x : event.clientX; var clientY = event.center ? event.center.y : event.clientY; var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); var item = this.itemSet.itemFromTarget(event); var group = this.itemSet.groupFromTarget(event); var customTime = CustomTime.customTimeFromTarget(event); var snap = this.itemSet.options.snap || null; var scale = this.body.util.getScale(); var step = this.body.util.getStep(); var time = this._toTime(x); var snappedTime = snap ? snap(time, scale, step) : time; var element = util.getTarget(event); var what = null; if (item != null) { what = 'item'; } else if (customTime != null) { what = 'custom-time'; } else if (util.hasParent(element, this.timeAxis.dom.foreground)) { what = 'axis'; } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { what = 'axis'; } else if (util.hasParent(element, this.itemSet.dom.labelSet)) { what = 'group-label'; } else if (util.hasParent(element, this.currentTime.bar)) { what = 'current-time'; } else if (util.hasParent(element, this.dom.center)) { what = 'background'; } return { event: event, item: item ? item.id : null, group: group ? group.groupId : null, what: what, pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, x: x, y: y, time: time, snappedTime: snappedTime }; }; module.exports = Timeline; /***/ }, /* 20 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var Component = __webpack_require__(21); var moment = __webpack_require__(2); var locales = __webpack_require__(22); /** * A current time bar * @param {{range: Range, dom: Object, domProps: Object}} body * @param {Object} [options] Available parameters: * {Boolean} [showCurrentTime] * @constructor CurrentTime * @extends Component */ function CurrentTime(body, options) { this.body = body; // default options this.defaultOptions = { showCurrentTime: true, locales: locales, locale: 'en' }; this.options = util.extend({}, this.defaultOptions); this.offset = 0; this._create(); this.setOptions(options); } CurrentTime.prototype = new Component(); /** * Create the HTML DOM for the current time bar * @private */ CurrentTime.prototype._create = function () { var bar = document.createElement('div'); bar.className = 'vis-current-time'; bar.style.position = 'absolute'; bar.style.top = '0px'; bar.style.height = '100%'; this.bar = bar; }; /** * Destroy the CurrentTime bar */ CurrentTime.prototype.destroy = function () { this.options.showCurrentTime = false; this.redraw(); // will remove the bar from the DOM and stop refreshing this.body = null; }; /** * Set options for the component. Options will be merged in current options. * @param {Object} options Available parameters: * {boolean} [showCurrentTime] */ CurrentTime.prototype.setOptions = function (options) { if (options) { // copy all options that we know util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); } }; /** * Repaint the component * @return {boolean} Returns true if the component is resized */ CurrentTime.prototype.redraw = function () { if (this.options.showCurrentTime) { var parent = this.body.dom.backgroundVertical; if (this.bar.parentNode != parent) { // attach to the dom if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); } parent.appendChild(this.bar); this.start(); } var now = new Date(new Date().valueOf() + this.offset); var x = this.body.util.toScreen(now); var locale = this.options.locales[this.options.locale]; if (!locale) { if (!this.warned) { console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); this.warned = true; } locale = this.options.locales['en']; // fall back on english when not available } var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); title = title.charAt(0).toUpperCase() + title.substring(1); this.bar.style.left = x + 'px'; this.bar.title = title; } else { // remove the line from the DOM if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); } this.stop(); } return false; }; /** * Start auto refreshing the current time bar */ CurrentTime.prototype.start = function () { var me = this; function update() { me.stop(); // determine interval to refresh var scale = me.body.range.conversion(me.body.domProps.center.width).scale; var interval = 1 / scale / 10; if (interval < 30) interval = 30; if (interval > 1000) interval = 1000; me.redraw(); // start a renderTimer to adjust for the new time me.currentTimeTimer = setTimeout(update, interval); } update(); }; /** * Stop auto refreshing the current time bar */ CurrentTime.prototype.stop = function () { if (this.currentTimeTimer !== undefined) { clearTimeout(this.currentTimeTimer); delete this.currentTimeTimer; } }; /** * Set a current time. This can be used for example to ensure that a client's * time is synchronized with a shared server time. * @param {Date | String | Number} time A Date, unix timestamp, or * ISO date string. */ CurrentTime.prototype.setCurrentTime = function (time) { var t = util.convert(time, 'Date').valueOf(); var now = new Date().valueOf(); this.offset = t - now; this.redraw(); }; /** * Get the current time. * @return {Date} Returns the current time. */ CurrentTime.prototype.getCurrentTime = function () { return new Date(new Date().valueOf() + this.offset); }; module.exports = CurrentTime; /***/ }, /* 21 */ /***/ function(module, exports) { /** * Prototype for visual components * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] * @param {Object} [options] */ "use strict"; function Component(body, options) { this.options = null; this.props = null; } /** * Set options for the component. The new options will be merged into the * current options. * @param {Object} options */ Component.prototype.setOptions = function (options) { if (options) { util.extend(this.options, options); } }; /** * Repaint the component * @return {boolean} Returns true if the component is resized */ Component.prototype.redraw = function () { // should be implemented by the component return false; }; /** * Destroy the component. Cleanup DOM and event listeners */ Component.prototype.destroy = function () {}; /** * Test whether the component is resized since the last time _isResized() was * called. * @return {Boolean} Returns true if the component is resized * @protected */ Component.prototype._isResized = function () { var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; this.props._previousWidth = this.props.width; this.props._previousHeight = this.props.height; return resized; }; module.exports = Component; // should be implemented by the component /***/ }, /* 22 */ /***/ function(module, exports) { // English 'use strict'; exports['en'] = { current: 'current', time: 'time' }; exports['en_EN'] = exports['en']; exports['en_US'] = exports['en']; // Dutch exports['nl'] = { current: 'huidige', time: 'tijd' }; exports['nl_NL'] = exports['nl']; exports['nl_BE'] = exports['nl']; /***/ }, /* 23 */ /***/ function(module, exports, __webpack_require__) { // Only load hammer.js when in a browser environment // (loading hammer.js in a node.js environment gives errors) 'use strict'; if (typeof window !== 'undefined') { var propagating = __webpack_require__(24); var Hammer = window['Hammer'] || __webpack_require__(25); module.exports = propagating(Hammer, { preventDefault: 'mouse' }); } else { module.exports = function () { throw Error('hammer.js is only available in a browser, not in node.js.'); }; } /***/ }, /* 24 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; (function (factory) { if (true) { // AMD. Register as an anonymous module. !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) window.propagating = factory(); } }(function () { var _firstTarget = null; // singleton, will contain the target element where the touch event started var _processing = false; // singleton, true when a touch event is being handled /** * Extend an Hammer.js instance with event propagation. * * Features: * - Events emitted by hammer will propagate in order from child to parent * elements. * - Events are extended with a function `event.stopPropagation()` to stop * propagation to parent elements. * - An option `preventDefault` to stop all default browser behavior. * * Usage: * var hammer = propagatingHammer(new Hammer(element)); * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); * * @param {Hammer.Manager} hammer An hammer instance. * @param {Object} [options] Available options: * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. * Enforce preventing the default browser behavior. * Cannot be set to `false`. * @return {Hammer.Manager} Returns the same hammer instance with extended * functionality */ return function propagating(hammer, options) { var _options = options || { preventDefault: false }; if (hammer.Manager) { // This looks like the Hammer constructor. // Overload the constructors with our own. var Hammer = hammer; var PropagatingHammer = function(element, options) { var o = Object.create(_options); if (options) Hammer.extend(o, options); return propagating(new Hammer(element, o), o); }; Hammer.extend(PropagatingHammer, Hammer); PropagatingHammer.Manager = function (element, options) { var o = Object.create(_options); if (options) Hammer.extend(o, options); return propagating(new Hammer.Manager(element, o), o); }; return PropagatingHammer; } // create a wrapper object which will override the functions // `on`, `off`, `destroy`, and `emit` of the hammer instance var wrapper = Object.create(hammer); // attach to DOM element var element = hammer.element; element.hammer = wrapper; // register an event to catch the start of a gesture and store the // target in a singleton hammer.on('hammer.input', function (event) { if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { event.preventDefault(); } if (event.isFirst) { _firstTarget = event.target; } }); /** @type {Object.>} */ wrapper._handlers = {}; /** * Register a handler for one or multiple events * @param {String} events A space separated string with events * @param {function} handler A callback function, called as handler(event) * @returns {Hammer.Manager} Returns the hammer instance */ wrapper.on = function (events, handler) { // register the handler split(events).forEach(function (event) { var _handlers = wrapper._handlers[event]; if (!_handlers) { wrapper._handlers[event] = _handlers = []; // register the static, propagated handler hammer.on(event, propagatedHandler); } _handlers.push(handler); }); return wrapper; }; /** * Unregister a handler for one or multiple events * @param {String} events A space separated string with events * @param {function} [handler] Optional. The registered handler. If not * provided, all handlers for given events * are removed. * @returns {Hammer.Manager} Returns the hammer instance */ wrapper.off = function (events, handler) { // unregister the handler split(events).forEach(function (event) { var _handlers = wrapper._handlers[event]; if (_handlers) { _handlers = handler ? _handlers.filter(function (h) { return h !== handler; }) : []; if (_handlers.length > 0) { wrapper._handlers[event] = _handlers; } else { // remove static, propagated handler hammer.off(event, propagatedHandler); delete wrapper._handlers[event]; } } }); return wrapper; }; /** * Emit to the event listeners * @param {string} eventType * @param {Event} event */ wrapper.emit = function(eventType, event) { _firstTarget = event.target; hammer.emit(eventType, event); }; wrapper.destroy = function () { // Detach from DOM element delete hammer.element.hammer; // clear all handlers wrapper._handlers = {}; // call original hammer destroy hammer.destroy(); }; // split a string with space separated words function split(events) { return events.match(/[^ ]+/g); } /** * A static event handler, applying event propagation. * @param {Object} event */ function propagatedHandler(event) { // let only a single hammer instance handle this event if (event.type !== 'hammer.input') { // it is possible that the same srcEvent is used with multiple hammer events, // we keep track on which events are handled in an object _handled if (!event.srcEvent._handled) { event.srcEvent._handled = {}; } if (event.srcEvent._handled[event.type]) { return; } else { event.srcEvent._handled[event.type] = true; } } // attach a stopPropagation function to the event var stopped = false; event.stopPropagation = function () { stopped = true; }; // attach firstTarget property to the event event.firstTarget = _firstTarget; // propagate over all elements (until stopped) var elem = _firstTarget; while (elem && !stopped) { var _handlers = elem.hammer && elem.hammer._handlers[event.type]; if (_handlers) { for (var i = 0; i < _handlers.length && !stopped; i++) { _handlers[i](event); } } elem = elem.parentNode; } } return wrapper; }; })); /***/ }, /* 25 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 * http://hammerjs.github.io/ * * Copyright (c) 2014 Jorik Tangelder; * Licensed under the MIT license */ (function(window, document, exportName, undefined) { 'use strict'; var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; var TEST_ELEMENT = document.createElement('div'); var TYPE_FUNCTION = 'function'; var round = Math.round; var abs = Math.abs; var now = Date.now; /** * set a timeout with a given scope * @param {Function} fn * @param {Number} timeout * @param {Object} context * @returns {number} */ function setTimeoutContext(fn, timeout, context) { return setTimeout(bindFn(fn, context), timeout); } /** * if the argument is an array, we want to execute the fn on each entry * if it aint an array we don't want to do a thing. * this is used by all the methods that accept a single and array argument. * @param {*|Array} arg * @param {String} fn * @param {Object} [context] * @returns {Boolean} */ function invokeArrayArg(arg, fn, context) { if (Array.isArray(arg)) { each(arg, context[fn], context); return true; } return false; } /** * walk objects and arrays * @param {Object} obj * @param {Function} iterator * @param {Object} context */ function each(obj, iterator, context) { var i; if (!obj) { return; } if (obj.forEach) { obj.forEach(iterator, context); } else if (obj.length !== undefined) { i = 0; while (i < obj.length) { iterator.call(context, obj[i], i, obj); i++; } } else { for (i in obj) { obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); } } } /** * extend object. * means that properties in dest will be overwritten by the ones in src. * @param {Object} dest * @param {Object} src * @param {Boolean} [merge] * @returns {Object} dest */ function extend(dest, src, merge) { var keys = Object.keys(src); var i = 0; while (i < keys.length) { if (!merge || (merge && dest[keys[i]] === undefined)) { dest[keys[i]] = src[keys[i]]; } i++; } return dest; } /** * merge the values from src in the dest. * means that properties that exist in dest will not be overwritten by src * @param {Object} dest * @param {Object} src * @returns {Object} dest */ function merge(dest, src) { return extend(dest, src, true); } /** * simple class inheritance * @param {Function} child * @param {Function} base * @param {Object} [properties] */ function inherit(child, base, properties) { var baseP = base.prototype, childP; childP = child.prototype = Object.create(baseP); childP.constructor = child; childP._super = baseP; if (properties) { extend(childP, properties); } } /** * simple function bind * @param {Function} fn * @param {Object} context * @returns {Function} */ function bindFn(fn, context) { return function boundFn() { return fn.apply(context, arguments); }; } /** * let a boolean value also be a function that must return a boolean * this first item in args will be used as the context * @param {Boolean|Function} val * @param {Array} [args] * @returns {Boolean} */ function boolOrFn(val, args) { if (typeof val == TYPE_FUNCTION) { return val.apply(args ? args[0] || undefined : undefined, args); } return val; } /** * use the val2 when val1 is undefined * @param {*} val1 * @param {*} val2 * @returns {*} */ function ifUndefined(val1, val2) { return (val1 === undefined) ? val2 : val1; } /** * addEventListener with multiple events at once * @param {EventTarget} target * @param {String} types * @param {Function} handler */ function addEventListeners(target, types, handler) { each(splitStr(types), function(type) { target.addEventListener(type, handler, false); }); } /** * removeEventListener with multiple events at once * @param {EventTarget} target * @param {String} types * @param {Function} handler */ function removeEventListeners(target, types, handler) { each(splitStr(types), function(type) { target.removeEventListener(type, handler, false); }); } /** * find if a node is in the given parent * @method hasParent * @param {HTMLElement} node * @param {HTMLElement} parent * @return {Boolean} found */ function hasParent(node, parent) { while (node) { if (node == parent) { return true; } node = node.parentNode; } return false; } /** * small indexOf wrapper * @param {String} str * @param {String} find * @returns {Boolean} found */ function inStr(str, find) { return str.indexOf(find) > -1; } /** * split string on whitespace * @param {String} str * @returns {Array} words */ function splitStr(str) { return str.trim().split(/\s+/g); } /** * find if a array contains the object using indexOf or a simple polyFill * @param {Array} src * @param {String} find * @param {String} [findByKey] * @return {Boolean|Number} false when not found, or the index */ function inArray(src, find, findByKey) { if (src.indexOf && !findByKey) { return src.indexOf(find); } else { var i = 0; while (i < src.length) { if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { return i; } i++; } return -1; } } /** * convert array-like objects to real arrays * @param {Object} obj * @returns {Array} */ function toArray(obj) { return Array.prototype.slice.call(obj, 0); } /** * unique array with objects based on a key (like 'id') or just by the array's value * @param {Array} src [{id:1},{id:2},{id:1}] * @param {String} [key] * @param {Boolean} [sort=False] * @returns {Array} [{id:1},{id:2}] */ function uniqueArray(src, key, sort) { var results = []; var values = []; var i = 0; while (i < src.length) { var val = key ? src[i][key] : src[i]; if (inArray(values, val) < 0) { results.push(src[i]); } values[i] = val; i++; } if (sort) { if (!key) { results = results.sort(); } else { results = results.sort(function sortUniqueArray(a, b) { return a[key] > b[key]; }); } } return results; } /** * get the prefixed property * @param {Object} obj * @param {String} property * @returns {String|Undefined} prefixed */ function prefixed(obj, property) { var prefix, prop; var camelProp = property[0].toUpperCase() + property.slice(1); var i = 0; while (i < VENDOR_PREFIXES.length) { prefix = VENDOR_PREFIXES[i]; prop = (prefix) ? prefix + camelProp : property; if (prop in obj) { return prop; } i++; } return undefined; } /** * get a unique id * @returns {number} uniqueId */ var _uniqueId = 1; function uniqueId() { return _uniqueId++; } /** * get the window object of an element * @param {HTMLElement} element * @returns {DocumentView|Window} */ function getWindowForElement(element) { var doc = element.ownerDocument; return (doc.defaultView || doc.parentWindow); } var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; var SUPPORT_TOUCH = ('ontouchstart' in window); var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); var INPUT_TYPE_TOUCH = 'touch'; var INPUT_TYPE_PEN = 'pen'; var INPUT_TYPE_MOUSE = 'mouse'; var INPUT_TYPE_KINECT = 'kinect'; var COMPUTE_INTERVAL = 25; var INPUT_START = 1; var INPUT_MOVE = 2; var INPUT_END = 4; var INPUT_CANCEL = 8; var DIRECTION_NONE = 1; var DIRECTION_LEFT = 2; var DIRECTION_RIGHT = 4; var DIRECTION_UP = 8; var DIRECTION_DOWN = 16; var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; var PROPS_XY = ['x', 'y']; var PROPS_CLIENT_XY = ['clientX', 'clientY']; /** * create new input type manager * @param {Manager} manager * @param {Function} callback * @returns {Input} * @constructor */ function Input(manager, callback) { var self = this; this.manager = manager; this.callback = callback; this.element = manager.element; this.target = manager.options.inputTarget; // smaller wrapper around the handler, for the scope and the enabled state of the manager, // so when disabled the input events are completely bypassed. this.domHandler = function(ev) { if (boolOrFn(manager.options.enable, [manager])) { self.handler(ev); } }; this.init(); } Input.prototype = { /** * should handle the inputEvent data and trigger the callback * @virtual */ handler: function() { }, /** * bind the events */ init: function() { this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); }, /** * unbind the events */ destroy: function() { this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); } }; /** * create new input type manager * called by the Manager constructor * @param {Hammer} manager * @returns {Input} */ function createInputInstance(manager) { var Type; var inputClass = manager.options.inputClass; if (inputClass) { Type = inputClass; } else if (SUPPORT_POINTER_EVENTS) { Type = PointerEventInput; } else if (SUPPORT_ONLY_TOUCH) { Type = TouchInput; } else if (!SUPPORT_TOUCH) { Type = MouseInput; } else { Type = TouchMouseInput; } return new (Type)(manager, inputHandler); } /** * handle input events * @param {Manager} manager * @param {String} eventType * @param {Object} input */ function inputHandler(manager, eventType, input) { var pointersLen = input.pointers.length; var changedPointersLen = input.changedPointers.length; var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); input.isFirst = !!isFirst; input.isFinal = !!isFinal; if (isFirst) { manager.session = {}; } // source event is the normalized value of the domEvents // like 'touchstart, mouseup, pointerdown' input.eventType = eventType; // compute scale, rotation etc computeInputData(manager, input); // emit secret event manager.emit('hammer.input', input); manager.recognize(input); manager.session.prevInput = input; } /** * extend the data with some usable properties like scale, rotate, velocity etc * @param {Object} manager * @param {Object} input */ function computeInputData(manager, input) { var session = manager.session; var pointers = input.pointers; var pointersLength = pointers.length; // store the first input to calculate the distance and direction if (!session.firstInput) { session.firstInput = simpleCloneInputData(input); } // to compute scale and rotation we need to store the multiple touches if (pointersLength > 1 && !session.firstMultiple) { session.firstMultiple = simpleCloneInputData(input); } else if (pointersLength === 1) { session.firstMultiple = false; } var firstInput = session.firstInput; var firstMultiple = session.firstMultiple; var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; var center = input.center = getCenter(pointers); input.timeStamp = now(); input.deltaTime = input.timeStamp - firstInput.timeStamp; input.angle = getAngle(offsetCenter, center); input.distance = getDistance(offsetCenter, center); computeDeltaXY(session, input); input.offsetDirection = getDirection(input.deltaX, input.deltaY); input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; computeIntervalInputData(session, input); // find the correct target var target = manager.element; if (hasParent(input.srcEvent.target, target)) { target = input.srcEvent.target; } input.target = target; } function computeDeltaXY(session, input) { var center = input.center; var offset = session.offsetDelta || {}; var prevDelta = session.prevDelta || {}; var prevInput = session.prevInput || {}; if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { prevDelta = session.prevDelta = { x: prevInput.deltaX || 0, y: prevInput.deltaY || 0 }; offset = session.offsetDelta = { x: center.x, y: center.y }; } input.deltaX = prevDelta.x + (center.x - offset.x); input.deltaY = prevDelta.y + (center.y - offset.y); } /** * velocity is calculated every x ms * @param {Object} session * @param {Object} input */ function computeIntervalInputData(session, input) { var last = session.lastInterval || input, deltaTime = input.timeStamp - last.timeStamp, velocity, velocityX, velocityY, direction; if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { var deltaX = last.deltaX - input.deltaX; var deltaY = last.deltaY - input.deltaY; var v = getVelocity(deltaTime, deltaX, deltaY); velocityX = v.x; velocityY = v.y; velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; direction = getDirection(deltaX, deltaY); session.lastInterval = input; } else { // use latest velocity info if it doesn't overtake a minimum period velocity = last.velocity; velocityX = last.velocityX; velocityY = last.velocityY; direction = last.direction; } input.velocity = velocity; input.velocityX = velocityX; input.velocityY = velocityY; input.direction = direction; } /** * create a simple clone from the input used for storage of firstInput and firstMultiple * @param {Object} input * @returns {Object} clonedInputData */ function simpleCloneInputData(input) { // make a simple copy of the pointers because we will get a reference if we don't // we only need clientXY for the calculations var pointers = []; var i = 0; while (i < input.pointers.length) { pointers[i] = { clientX: round(input.pointers[i].clientX), clientY: round(input.pointers[i].clientY) }; i++; } return { timeStamp: now(), pointers: pointers, center: getCenter(pointers), deltaX: input.deltaX, deltaY: input.deltaY }; } /** * get the center of all the pointers * @param {Array} pointers * @return {Object} center contains `x` and `y` properties */ function getCenter(pointers) { var pointersLength = pointers.length; // no need to loop when only one touch if (pointersLength === 1) { return { x: round(pointers[0].clientX), y: round(pointers[0].clientY) }; } var x = 0, y = 0, i = 0; while (i < pointersLength) { x += pointers[i].clientX; y += pointers[i].clientY; i++; } return { x: round(x / pointersLength), y: round(y / pointersLength) }; } /** * calculate the velocity between two points. unit is in px per ms. * @param {Number} deltaTime * @param {Number} x * @param {Number} y * @return {Object} velocity `x` and `y` */ function getVelocity(deltaTime, x, y) { return { x: x / deltaTime || 0, y: y / deltaTime || 0 }; } /** * get the direction between two points * @param {Number} x * @param {Number} y * @return {Number} direction */ function getDirection(x, y) { if (x === y) { return DIRECTION_NONE; } if (abs(x) >= abs(y)) { return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; } /** * calculate the absolute distance between two points * @param {Object} p1 {x, y} * @param {Object} p2 {x, y} * @param {Array} [props] containing x and y keys * @return {Number} distance */ function getDistance(p1, p2, props) { if (!props) { props = PROPS_XY; } var x = p2[props[0]] - p1[props[0]], y = p2[props[1]] - p1[props[1]]; return Math.sqrt((x * x) + (y * y)); } /** * calculate the angle between two coordinates * @param {Object} p1 * @param {Object} p2 * @param {Array} [props] containing x and y keys * @return {Number} angle */ function getAngle(p1, p2, props) { if (!props) { props = PROPS_XY; } var x = p2[props[0]] - p1[props[0]], y = p2[props[1]] - p1[props[1]]; return Math.atan2(y, x) * 180 / Math.PI; } /** * calculate the rotation degrees between two pointersets * @param {Array} start array of pointers * @param {Array} end array of pointers * @return {Number} rotation */ function getRotation(start, end) { return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); } /** * calculate the scale factor between two pointersets * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out * @param {Array} start array of pointers * @param {Array} end array of pointers * @return {Number} scale */ function getScale(start, end) { return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); } var MOUSE_INPUT_MAP = { mousedown: INPUT_START, mousemove: INPUT_MOVE, mouseup: INPUT_END }; var MOUSE_ELEMENT_EVENTS = 'mousedown'; var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; /** * Mouse events input * @constructor * @extends Input */ function MouseInput() { this.evEl = MOUSE_ELEMENT_EVENTS; this.evWin = MOUSE_WINDOW_EVENTS; this.allow = true; // used by Input.TouchMouse to disable mouse events this.pressed = false; // mousedown state Input.apply(this, arguments); } inherit(MouseInput, Input, { /** * handle mouse events * @param {Object} ev */ handler: function MEhandler(ev) { var eventType = MOUSE_INPUT_MAP[ev.type]; // on start we want to have the left mouse button down if (eventType & INPUT_START && ev.button === 0) { this.pressed = true; } if (eventType & INPUT_MOVE && ev.which !== 1) { eventType = INPUT_END; } // mouse must be down, and mouse events are allowed (see the TouchMouse input) if (!this.pressed || !this.allow) { return; } if (eventType & INPUT_END) { this.pressed = false; } this.callback(this.manager, eventType, { pointers: [ev], changedPointers: [ev], pointerType: INPUT_TYPE_MOUSE, srcEvent: ev }); } }); var POINTER_INPUT_MAP = { pointerdown: INPUT_START, pointermove: INPUT_MOVE, pointerup: INPUT_END, pointercancel: INPUT_CANCEL, pointerout: INPUT_CANCEL }; // in IE10 the pointer types is defined as an enum var IE10_POINTER_TYPE_ENUM = { 2: INPUT_TYPE_TOUCH, 3: INPUT_TYPE_PEN, 4: INPUT_TYPE_MOUSE, 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 }; var POINTER_ELEMENT_EVENTS = 'pointerdown'; var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; // IE10 has prefixed support, and case-sensitive if (window.MSPointerEvent) { POINTER_ELEMENT_EVENTS = 'MSPointerDown'; POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; } /** * Pointer events input * @constructor * @extends Input */ function PointerEventInput() { this.evEl = POINTER_ELEMENT_EVENTS; this.evWin = POINTER_WINDOW_EVENTS; Input.apply(this, arguments); this.store = (this.manager.session.pointerEvents = []); } inherit(PointerEventInput, Input, { /** * handle mouse events * @param {Object} ev */ handler: function PEhandler(ev) { var store = this.store; var removePointer = false; var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; var isTouch = (pointerType == INPUT_TYPE_TOUCH); // get index of the event in the store var storeIndex = inArray(store, ev.pointerId, 'pointerId'); // start and mouse must be down if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { if (storeIndex < 0) { store.push(ev); storeIndex = store.length - 1; } } else if (eventType & (INPUT_END | INPUT_CANCEL)) { removePointer = true; } // it not found, so the pointer hasn't been down (so it's probably a hover) if (storeIndex < 0) { return; } // update the event in the store store[storeIndex] = ev; this.callback(this.manager, eventType, { pointers: store, changedPointers: [ev], pointerType: pointerType, srcEvent: ev }); if (removePointer) { // remove from the store store.splice(storeIndex, 1); } } }); var SINGLE_TOUCH_INPUT_MAP = { touchstart: INPUT_START, touchmove: INPUT_MOVE, touchend: INPUT_END, touchcancel: INPUT_CANCEL }; var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** * Touch events input * @constructor * @extends Input */ function SingleTouchInput() { this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; this.started = false; Input.apply(this, arguments); } inherit(SingleTouchInput, Input, { handler: function TEhandler(ev) { var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; // should we handle the touch events? if (type === INPUT_START) { this.started = true; } if (!this.started) { return; } var touches = normalizeSingleTouches.call(this, ev, type); // when done, reset the started state if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { this.started = false; } this.callback(this.manager, type, { pointers: touches[0], changedPointers: touches[1], pointerType: INPUT_TYPE_TOUCH, srcEvent: ev }); } }); /** * @this {TouchInput} * @param {Object} ev * @param {Number} type flag * @returns {undefined|Array} [all, changed] */ function normalizeSingleTouches(ev, type) { var all = toArray(ev.touches); var changed = toArray(ev.changedTouches); if (type & (INPUT_END | INPUT_CANCEL)) { all = uniqueArray(all.concat(changed), 'identifier', true); } return [all, changed]; } var TOUCH_INPUT_MAP = { touchstart: INPUT_START, touchmove: INPUT_MOVE, touchend: INPUT_END, touchcancel: INPUT_CANCEL }; var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; /** * Multi-user touch events input * @constructor * @extends Input */ function TouchInput() { this.evTarget = TOUCH_TARGET_EVENTS; this.targetIds = {}; Input.apply(this, arguments); } inherit(TouchInput, Input, { handler: function MTEhandler(ev) { var type = TOUCH_INPUT_MAP[ev.type]; var touches = getTouches.call(this, ev, type); if (!touches) { return; } this.callback(this.manager, type, { pointers: touches[0], changedPointers: touches[1], pointerType: INPUT_TYPE_TOUCH, srcEvent: ev }); } }); /** * @this {TouchInput} * @param {Object} ev * @param {Number} type flag * @returns {undefined|Array} [all, changed] */ function getTouches(ev, type) { var allTouches = toArray(ev.touches); var targetIds = this.targetIds; // when there is only one touch, the process can be simplified if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { targetIds[allTouches[0].identifier] = true; return [allTouches, allTouches]; } var i, targetTouches, changedTouches = toArray(ev.changedTouches), changedTargetTouches = [], target = this.target; // get target touches from touches targetTouches = allTouches.filter(function(touch) { return hasParent(touch.target, target); }); // collect touches if (type === INPUT_START) { i = 0; while (i < targetTouches.length) { targetIds[targetTouches[i].identifier] = true; i++; } } // filter changed touches to only contain touches that exist in the collected target ids i = 0; while (i < changedTouches.length) { if (targetIds[changedTouches[i].identifier]) { changedTargetTouches.push(changedTouches[i]); } // cleanup removed touches if (type & (INPUT_END | INPUT_CANCEL)) { delete targetIds[changedTouches[i].identifier]; } i++; } if (!changedTargetTouches.length) { return; } return [ // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), changedTargetTouches ]; } /** * Combined touch and mouse input * * Touch has a higher priority then mouse, and while touching no mouse events are allowed. * This because touch devices also emit mouse events while doing a touch. * * @constructor * @extends Input */ function TouchMouseInput() { Input.apply(this, arguments); var handler = bindFn(this.handler, this); this.touch = new TouchInput(this.manager, handler); this.mouse = new MouseInput(this.manager, handler); } inherit(TouchMouseInput, Input, { /** * handle mouse and touch events * @param {Hammer} manager * @param {String} inputEvent * @param {Object} inputData */ handler: function TMEhandler(manager, inputEvent, inputData) { var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); // when we're in a touch event, so block all upcoming mouse events // most mobile browser also emit mouseevents, right after touchstart if (isTouch) { this.mouse.allow = false; } else if (isMouse && !this.mouse.allow) { return; } // reset the allowMouse when we're done if (inputEvent & (INPUT_END | INPUT_CANCEL)) { this.mouse.allow = true; } this.callback(manager, inputEvent, inputData); }, /** * remove the event listeners */ destroy: function destroy() { this.touch.destroy(); this.mouse.destroy(); } }); var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; // magical touchAction value var TOUCH_ACTION_COMPUTE = 'compute'; var TOUCH_ACTION_AUTO = 'auto'; var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented var TOUCH_ACTION_NONE = 'none'; var TOUCH_ACTION_PAN_X = 'pan-x'; var TOUCH_ACTION_PAN_Y = 'pan-y'; /** * Touch Action * sets the touchAction property or uses the js alternative * @param {Manager} manager * @param {String} value * @constructor */ function TouchAction(manager, value) { this.manager = manager; this.set(value); } TouchAction.prototype = { /** * set the touchAction value on the element or enable the polyfill * @param {String} value */ set: function(value) { // find out the touch-action by the event handlers if (value == TOUCH_ACTION_COMPUTE) { value = this.compute(); } if (NATIVE_TOUCH_ACTION) { this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; } this.actions = value.toLowerCase().trim(); }, /** * just re-set the touchAction value */ update: function() { this.set(this.manager.options.touchAction); }, /** * compute the value for the touchAction property based on the recognizer's settings * @returns {String} value */ compute: function() { var actions = []; each(this.manager.recognizers, function(recognizer) { if (boolOrFn(recognizer.options.enable, [recognizer])) { actions = actions.concat(recognizer.getTouchAction()); } }); return cleanTouchActions(actions.join(' ')); }, /** * this method is called on each input cycle and provides the preventing of the browser behavior * @param {Object} input */ preventDefaults: function(input) { // not needed with native support for the touchAction property if (NATIVE_TOUCH_ACTION) { return; } var srcEvent = input.srcEvent; var direction = input.offsetDirection; // if the touch action did prevented once this session if (this.manager.session.prevented) { srcEvent.preventDefault(); return; } var actions = this.actions; var hasNone = inStr(actions, TOUCH_ACTION_NONE); var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); if (hasNone || (hasPanY && direction & DIRECTION_HORIZONTAL) || (hasPanX && direction & DIRECTION_VERTICAL)) { return this.preventSrc(srcEvent); } }, /** * call preventDefault to prevent the browser's default behavior (scrolling in most cases) * @param {Object} srcEvent */ preventSrc: function(srcEvent) { this.manager.session.prevented = true; srcEvent.preventDefault(); } }; /** * when the touchActions are collected they are not a valid value, so we need to clean things up. * * @param {String} actions * @returns {*} */ function cleanTouchActions(actions) { // none if (inStr(actions, TOUCH_ACTION_NONE)) { return TOUCH_ACTION_NONE; } var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); // pan-x and pan-y can be combined if (hasPanX && hasPanY) { return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; } // pan-x OR pan-y if (hasPanX || hasPanY) { return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; } // manipulation if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { return TOUCH_ACTION_MANIPULATION; } return TOUCH_ACTION_AUTO; } /** * Recognizer flow explained; * * All recognizers have the initial state of POSSIBLE when a input session starts. * The definition of a input session is from the first input until the last input, with all it's movement in it. * * Example session for mouse-input: mousedown -> mousemove -> mouseup * * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed * which determines with state it should be. * * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to * POSSIBLE to give it another change on the next cycle. * * Possible * | * +-----+---------------+ * | | * +-----+-----+ | * | | | * Failed Cancelled | * +-------+------+ * | | * Recognized Began * | * Changed * | * Ended/Recognized */ var STATE_POSSIBLE = 1; var STATE_BEGAN = 2; var STATE_CHANGED = 4; var STATE_ENDED = 8; var STATE_RECOGNIZED = STATE_ENDED; var STATE_CANCELLED = 16; var STATE_FAILED = 32; /** * Recognizer * Every recognizer needs to extend from this class. * @constructor * @param {Object} options */ function Recognizer(options) { this.id = uniqueId(); this.manager = null; this.options = merge(options || {}, this.defaults); // default is enable true this.options.enable = ifUndefined(this.options.enable, true); this.state = STATE_POSSIBLE; this.simultaneous = {}; this.requireFail = []; } Recognizer.prototype = { /** * @virtual * @type {Object} */ defaults: {}, /** * set options * @param {Object} options * @return {Recognizer} */ set: function(options) { extend(this.options, options); // also update the touchAction, in case something changed about the directions/enabled state this.manager && this.manager.touchAction.update(); return this; }, /** * recognize simultaneous with an other recognizer. * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ recognizeWith: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { return this; } var simultaneous = this.simultaneous; otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); if (!simultaneous[otherRecognizer.id]) { simultaneous[otherRecognizer.id] = otherRecognizer; otherRecognizer.recognizeWith(this); } return this; }, /** * drop the simultaneous link. it doesnt remove the link on the other recognizer. * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ dropRecognizeWith: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { return this; } otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); delete this.simultaneous[otherRecognizer.id]; return this; }, /** * recognizer can only run when an other is failing * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ requireFailure: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { return this; } var requireFail = this.requireFail; otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); if (inArray(requireFail, otherRecognizer) === -1) { requireFail.push(otherRecognizer); otherRecognizer.requireFailure(this); } return this; }, /** * drop the requireFailure link. it does not remove the link on the other recognizer. * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ dropRequireFailure: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { return this; } otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); var index = inArray(this.requireFail, otherRecognizer); if (index > -1) { this.requireFail.splice(index, 1); } return this; }, /** * has require failures boolean * @returns {boolean} */ hasRequireFailures: function() { return this.requireFail.length > 0; }, /** * if the recognizer can recognize simultaneous with an other recognizer * @param {Recognizer} otherRecognizer * @returns {Boolean} */ canRecognizeWith: function(otherRecognizer) { return !!this.simultaneous[otherRecognizer.id]; }, /** * You should use `tryEmit` instead of `emit` directly to check * that all the needed recognizers has failed before emitting. * @param {Object} input */ emit: function(input) { var self = this; var state = this.state; function emit(withState) { self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); } // 'panstart' and 'panmove' if (state < STATE_ENDED) { emit(true); } emit(); // simple 'eventName' events // panend and pancancel if (state >= STATE_ENDED) { emit(true); } }, /** * Check that all the require failure recognizers has failed, * if true, it emits a gesture event, * otherwise, setup the state to FAILED. * @param {Object} input */ tryEmit: function(input) { if (this.canEmit()) { return this.emit(input); } // it's failing anyway this.state = STATE_FAILED; }, /** * can we emit? * @returns {boolean} */ canEmit: function() { var i = 0; while (i < this.requireFail.length) { if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { return false; } i++; } return true; }, /** * update the recognizer * @param {Object} inputData */ recognize: function(inputData) { // make a new copy of the inputData // so we can change the inputData without messing up the other recognizers var inputDataClone = extend({}, inputData); // is is enabled and allow recognizing? if (!boolOrFn(this.options.enable, [this, inputDataClone])) { this.reset(); this.state = STATE_FAILED; return; } // reset when we've reached the end if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { this.state = STATE_POSSIBLE; } this.state = this.process(inputDataClone); // the recognizer has recognized a gesture // so trigger an event if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { this.tryEmit(inputDataClone); } }, /** * return the state of the recognizer * the actual recognizing happens in this method * @virtual * @param {Object} inputData * @returns {Const} STATE */ process: function(inputData) { }, // jshint ignore:line /** * return the preferred touch-action * @virtual * @returns {Array} */ getTouchAction: function() { }, /** * called when the gesture isn't allowed to recognize * like when another is being recognized or it is disabled * @virtual */ reset: function() { } }; /** * get a usable string, used as event postfix * @param {Const} state * @returns {String} state */ function stateStr(state) { if (state & STATE_CANCELLED) { return 'cancel'; } else if (state & STATE_ENDED) { return 'end'; } else if (state & STATE_CHANGED) { return 'move'; } else if (state & STATE_BEGAN) { return 'start'; } return ''; } /** * direction cons to string * @param {Const} direction * @returns {String} */ function directionStr(direction) { if (direction == DIRECTION_DOWN) { return 'down'; } else if (direction == DIRECTION_UP) { return 'up'; } else if (direction == DIRECTION_LEFT) { return 'left'; } else if (direction == DIRECTION_RIGHT) { return 'right'; } return ''; } /** * get a recognizer by name if it is bound to a manager * @param {Recognizer|String} otherRecognizer * @param {Recognizer} recognizer * @returns {Recognizer} */ function getRecognizerByNameIfManager(otherRecognizer, recognizer) { var manager = recognizer.manager; if (manager) { return manager.get(otherRecognizer); } return otherRecognizer; } /** * This recognizer is just used as a base for the simple attribute recognizers. * @constructor * @extends Recognizer */ function AttrRecognizer() { Recognizer.apply(this, arguments); } inherit(AttrRecognizer, Recognizer, { /** * @namespace * @memberof AttrRecognizer */ defaults: { /** * @type {Number} * @default 1 */ pointers: 1 }, /** * Used to check if it the recognizer receives valid input, like input.distance > 10. * @memberof AttrRecognizer * @param {Object} input * @returns {Boolean} recognized */ attrTest: function(input) { var optionPointers = this.options.pointers; return optionPointers === 0 || input.pointers.length === optionPointers; }, /** * Process the input and return the state for the recognizer * @memberof AttrRecognizer * @param {Object} input * @returns {*} State */ process: function(input) { var state = this.state; var eventType = input.eventType; var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); var isValid = this.attrTest(input); // on cancel input and we've recognized before, return STATE_CANCELLED if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { return state | STATE_CANCELLED; } else if (isRecognized || isValid) { if (eventType & INPUT_END) { return state | STATE_ENDED; } else if (!(state & STATE_BEGAN)) { return STATE_BEGAN; } return state | STATE_CHANGED; } return STATE_FAILED; } }); /** * Pan * Recognized when the pointer is down and moved in the allowed direction. * @constructor * @extends AttrRecognizer */ function PanRecognizer() { AttrRecognizer.apply(this, arguments); this.pX = null; this.pY = null; } inherit(PanRecognizer, AttrRecognizer, { /** * @namespace * @memberof PanRecognizer */ defaults: { event: 'pan', threshold: 10, pointers: 1, direction: DIRECTION_ALL }, getTouchAction: function() { var direction = this.options.direction; var actions = []; if (direction & DIRECTION_HORIZONTAL) { actions.push(TOUCH_ACTION_PAN_Y); } if (direction & DIRECTION_VERTICAL) { actions.push(TOUCH_ACTION_PAN_X); } return actions; }, directionTest: function(input) { var options = this.options; var hasMoved = true; var distance = input.distance; var direction = input.direction; var x = input.deltaX; var y = input.deltaY; // lock to axis? if (!(direction & options.direction)) { if (options.direction & DIRECTION_HORIZONTAL) { direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; hasMoved = x != this.pX; distance = Math.abs(input.deltaX); } else { direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; hasMoved = y != this.pY; distance = Math.abs(input.deltaY); } } input.direction = direction; return hasMoved && distance > options.threshold && direction & options.direction; }, attrTest: function(input) { return AttrRecognizer.prototype.attrTest.call(this, input) && (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); }, emit: function(input) { this.pX = input.deltaX; this.pY = input.deltaY; var direction = directionStr(input.direction); if (direction) { this.manager.emit(this.options.event + direction, input); } this._super.emit.call(this, input); } }); /** * Pinch * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). * @constructor * @extends AttrRecognizer */ function PinchRecognizer() { AttrRecognizer.apply(this, arguments); } inherit(PinchRecognizer, AttrRecognizer, { /** * @namespace * @memberof PinchRecognizer */ defaults: { event: 'pinch', threshold: 0, pointers: 2 }, getTouchAction: function() { return [TOUCH_ACTION_NONE]; }, attrTest: function(input) { return this._super.attrTest.call(this, input) && (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); }, emit: function(input) { this._super.emit.call(this, input); if (input.scale !== 1) { var inOut = input.scale < 1 ? 'in' : 'out'; this.manager.emit(this.options.event + inOut, input); } } }); /** * Press * Recognized when the pointer is down for x ms without any movement. * @constructor * @extends Recognizer */ function PressRecognizer() { Recognizer.apply(this, arguments); this._timer = null; this._input = null; } inherit(PressRecognizer, Recognizer, { /** * @namespace * @memberof PressRecognizer */ defaults: { event: 'press', pointers: 1, time: 500, // minimal time of the pointer to be pressed threshold: 5 // a minimal movement is ok, but keep it low }, getTouchAction: function() { return [TOUCH_ACTION_AUTO]; }, process: function(input) { var options = this.options; var validPointers = input.pointers.length === options.pointers; var validMovement = input.distance < options.threshold; var validTime = input.deltaTime > options.time; this._input = input; // we only allow little movement // and we've reached an end event, so a tap is possible if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { this.reset(); } else if (input.eventType & INPUT_START) { this.reset(); this._timer = setTimeoutContext(function() { this.state = STATE_RECOGNIZED; this.tryEmit(); }, options.time, this); } else if (input.eventType & INPUT_END) { return STATE_RECOGNIZED; } return STATE_FAILED; }, reset: function() { clearTimeout(this._timer); }, emit: function(input) { if (this.state !== STATE_RECOGNIZED) { return; } if (input && (input.eventType & INPUT_END)) { this.manager.emit(this.options.event + 'up', input); } else { this._input.timeStamp = now(); this.manager.emit(this.options.event, this._input); } } }); /** * Rotate * Recognized when two or more pointer are moving in a circular motion. * @constructor * @extends AttrRecognizer */ function RotateRecognizer() { AttrRecognizer.apply(this, arguments); } inherit(RotateRecognizer, AttrRecognizer, { /** * @namespace * @memberof RotateRecognizer */ defaults: { event: 'rotate', threshold: 0, pointers: 2 }, getTouchAction: function() { return [TOUCH_ACTION_NONE]; }, attrTest: function(input) { return this._super.attrTest.call(this, input) && (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); } }); /** * Swipe * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. * @constructor * @extends AttrRecognizer */ function SwipeRecognizer() { AttrRecognizer.apply(this, arguments); } inherit(SwipeRecognizer, AttrRecognizer, { /** * @namespace * @memberof SwipeRecognizer */ defaults: { event: 'swipe', threshold: 10, velocity: 0.65, direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, pointers: 1 }, getTouchAction: function() { return PanRecognizer.prototype.getTouchAction.call(this); }, attrTest: function(input) { var direction = this.options.direction; var velocity; if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { velocity = input.velocity; } else if (direction & DIRECTION_HORIZONTAL) { velocity = input.velocityX; } else if (direction & DIRECTION_VERTICAL) { velocity = input.velocityY; } return this._super.attrTest.call(this, input) && direction & input.direction && input.distance > this.options.threshold && abs(velocity) > this.options.velocity && input.eventType & INPUT_END; }, emit: function(input) { var direction = directionStr(input.direction); if (direction) { this.manager.emit(this.options.event + direction, input); } this.manager.emit(this.options.event, input); } }); /** * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur * between the given interval and position. The delay option can be used to recognize multi-taps without firing * a single tap. * * The eventData from the emitted event contains the property `tapCount`, which contains the amount of * multi-taps being recognized. * @constructor * @extends Recognizer */ function TapRecognizer() { Recognizer.apply(this, arguments); // previous time and center, // used for tap counting this.pTime = false; this.pCenter = false; this._timer = null; this._input = null; this.count = 0; } inherit(TapRecognizer, Recognizer, { /** * @namespace * @memberof PinchRecognizer */ defaults: { event: 'tap', pointers: 1, taps: 1, interval: 300, // max time between the multi-tap taps time: 250, // max time of the pointer to be down (like finger on the screen) threshold: 2, // a minimal movement is ok, but keep it low posThreshold: 10 // a multi-tap can be a bit off the initial position }, getTouchAction: function() { return [TOUCH_ACTION_MANIPULATION]; }, process: function(input) { var options = this.options; var validPointers = input.pointers.length === options.pointers; var validMovement = input.distance < options.threshold; var validTouchTime = input.deltaTime < options.time; this.reset(); if ((input.eventType & INPUT_START) && (this.count === 0)) { return this.failTimeout(); } // we only allow little movement // and we've reached an end event, so a tap is possible if (validMovement && validTouchTime && validPointers) { if (input.eventType != INPUT_END) { return this.failTimeout(); } var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; this.pTime = input.timeStamp; this.pCenter = input.center; if (!validMultiTap || !validInterval) { this.count = 1; } else { this.count += 1; } this._input = input; // if tap count matches we have recognized it, // else it has began recognizing... var tapCount = this.count % options.taps; if (tapCount === 0) { // no failing requirements, immediately trigger the tap event // or wait as long as the multitap interval to trigger if (!this.hasRequireFailures()) { return STATE_RECOGNIZED; } else { this._timer = setTimeoutContext(function() { this.state = STATE_RECOGNIZED; this.tryEmit(); }, options.interval, this); return STATE_BEGAN; } } } return STATE_FAILED; }, failTimeout: function() { this._timer = setTimeoutContext(function() { this.state = STATE_FAILED; }, this.options.interval, this); return STATE_FAILED; }, reset: function() { clearTimeout(this._timer); }, emit: function() { if (this.state == STATE_RECOGNIZED ) { this._input.tapCount = this.count; this.manager.emit(this.options.event, this._input); } } }); /** * Simple way to create an manager with a default set of recognizers. * @param {HTMLElement} element * @param {Object} [options] * @constructor */ function Hammer(element, options) { options = options || {}; options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); return new Manager(element, options); } /** * @const {string} */ Hammer.VERSION = '2.0.4'; /** * default settings * @namespace */ Hammer.defaults = { /** * set if DOM events are being triggered. * But this is slower and unused by simple implementations, so disabled by default. * @type {Boolean} * @default false */ domEvents: false, /** * The value for the touchAction property/fallback. * When set to `compute` it will magically set the correct value based on the added recognizers. * @type {String} * @default compute */ touchAction: TOUCH_ACTION_COMPUTE, /** * @type {Boolean} * @default true */ enable: true, /** * EXPERIMENTAL FEATURE -- can be removed/changed * Change the parent input target element. * If Null, then it is being set the to main element. * @type {Null|EventTarget} * @default null */ inputTarget: null, /** * force an input class * @type {Null|Function} * @default null */ inputClass: null, /** * Default recognizer setup when calling `Hammer()` * When creating a new Manager these will be skipped. * @type {Array} */ preset: [ // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] [RotateRecognizer, { enable: false }], [PinchRecognizer, { enable: false }, ['rotate']], [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], [TapRecognizer], [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], [PressRecognizer] ], /** * Some CSS properties can be used to improve the working of Hammer. * Add them to this method and they will be set when creating a new Manager. * @namespace */ cssProps: { /** * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. * @type {String} * @default 'none' */ userSelect: 'none', /** * Disable the Windows Phone grippers when pressing an element. * @type {String} * @default 'none' */ touchSelect: 'none', /** * Disables the default callout shown when you touch and hold a touch target. * On iOS, when you touch and hold a touch target such as a link, Safari displays * a callout containing information about the link. This property allows you to disable that callout. * @type {String} * @default 'none' */ touchCallout: 'none', /** * Specifies whether zooming is enabled. Used by IE10> * @type {String} * @default 'none' */ contentZooming: 'none', /** * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. * @type {String} * @default 'none' */ userDrag: 'none', /** * Overrides the highlight color shown when the user taps a link or a JavaScript * clickable element in iOS. This property obeys the alpha value, if specified. * @type {String} * @default 'rgba(0,0,0,0)' */ tapHighlightColor: 'rgba(0,0,0,0)' } }; var STOP = 1; var FORCED_STOP = 2; /** * Manager * @param {HTMLElement} element * @param {Object} [options] * @constructor */ function Manager(element, options) { options = options || {}; this.options = merge(options, Hammer.defaults); this.options.inputTarget = this.options.inputTarget || element; this.handlers = {}; this.session = {}; this.recognizers = []; this.element = element; this.input = createInputInstance(this); this.touchAction = new TouchAction(this, this.options.touchAction); toggleCssProps(this, true); each(options.recognizers, function(item) { var recognizer = this.add(new (item[0])(item[1])); item[2] && recognizer.recognizeWith(item[2]); item[3] && recognizer.requireFailure(item[3]); }, this); } Manager.prototype = { /** * set options * @param {Object} options * @returns {Manager} */ set: function(options) { extend(this.options, options); // Options that need a little more setup if (options.touchAction) { this.touchAction.update(); } if (options.inputTarget) { // Clean up existing event listeners and reinitialize this.input.destroy(); this.input.target = options.inputTarget; this.input.init(); } return this; }, /** * stop recognizing for this session. * This session will be discarded, when a new [input]start event is fired. * When forced, the recognizer cycle is stopped immediately. * @param {Boolean} [force] */ stop: function(force) { this.session.stopped = force ? FORCED_STOP : STOP; }, /** * run the recognizers! * called by the inputHandler function on every movement of the pointers (touches) * it walks through all the recognizers and tries to detect the gesture that is being made * @param {Object} inputData */ recognize: function(inputData) { var session = this.session; if (session.stopped) { return; } // run the touch-action polyfill this.touchAction.preventDefaults(inputData); var recognizer; var recognizers = this.recognizers; // this holds the recognizer that is being recognized. // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED // if no recognizer is detecting a thing, it is set to `null` var curRecognizer = session.curRecognizer; // reset when the last recognizer is recognized // or when we're in a new session if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { curRecognizer = session.curRecognizer = null; } var i = 0; while (i < recognizers.length) { recognizer = recognizers[i]; // find out if we are allowed try to recognize the input for this one. // 1. allow if the session is NOT forced stopped (see the .stop() method) // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one // that is being recognized. // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. // this can be setup with the `recognizeWith()` method on the recognizer. if (session.stopped !== FORCED_STOP && ( // 1 !curRecognizer || recognizer == curRecognizer || // 2 recognizer.canRecognizeWith(curRecognizer))) { // 3 recognizer.recognize(inputData); } else { recognizer.reset(); } // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the // current active recognizer. but only if we don't already have an active recognizer if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { curRecognizer = session.curRecognizer = recognizer; } i++; } }, /** * get a recognizer by its event name. * @param {Recognizer|String} recognizer * @returns {Recognizer|Null} */ get: function(recognizer) { if (recognizer instanceof Recognizer) { return recognizer; } var recognizers = this.recognizers; for (var i = 0; i < recognizers.length; i++) { if (recognizers[i].options.event == recognizer) { return recognizers[i]; } } return null; }, /** * add a recognizer to the manager * existing recognizers with the same event name will be removed * @param {Recognizer} recognizer * @returns {Recognizer|Manager} */ add: function(recognizer) { if (invokeArrayArg(recognizer, 'add', this)) { return this; } // remove existing var existing = this.get(recognizer.options.event); if (existing) { this.remove(existing); } this.recognizers.push(recognizer); recognizer.manager = this; this.touchAction.update(); return recognizer; }, /** * remove a recognizer by name or instance * @param {Recognizer|String} recognizer * @returns {Manager} */ remove: function(recognizer) { if (invokeArrayArg(recognizer, 'remove', this)) { return this; } var recognizers = this.recognizers; recognizer = this.get(recognizer); recognizers.splice(inArray(recognizers, recognizer), 1); this.touchAction.update(); return this; }, /** * bind event * @param {String} events * @param {Function} handler * @returns {EventEmitter} this */ on: function(events, handler) { var handlers = this.handlers; each(splitStr(events), function(event) { handlers[event] = handlers[event] || []; handlers[event].push(handler); }); return this; }, /** * unbind event, leave emit blank to remove all handlers * @param {String} events * @param {Function} [handler] * @returns {EventEmitter} this */ off: function(events, handler) { var handlers = this.handlers; each(splitStr(events), function(event) { if (!handler) { delete handlers[event]; } else { handlers[event].splice(inArray(handlers[event], handler), 1); } }); return this; }, /** * emit event to the listeners * @param {String} event * @param {Object} data */ emit: function(event, data) { // we also want to trigger dom events if (this.options.domEvents) { triggerDomEvent(event, data); } // no handlers, so skip it all var handlers = this.handlers[event] && this.handlers[event].slice(); if (!handlers || !handlers.length) { return; } data.type = event; data.preventDefault = function() { data.srcEvent.preventDefault(); }; var i = 0; while (i < handlers.length) { handlers[i](data); i++; } }, /** * destroy the manager and unbinds all events * it doesn't unbind dom events, that is the user own responsibility */ destroy: function() { this.element && toggleCssProps(this, false); this.handlers = {}; this.session = {}; this.input.destroy(); this.element = null; } }; /** * add/remove the css properties as defined in manager.options.cssProps * @param {Manager} manager * @param {Boolean} add */ function toggleCssProps(manager, add) { var element = manager.element; each(manager.options.cssProps, function(value, name) { element.style[prefixed(element.style, name)] = add ? value : ''; }); } /** * trigger dom event * @param {String} event * @param {Object} data */ function triggerDomEvent(event, data) { var gestureEvent = document.createEvent('Event'); gestureEvent.initEvent(event, true, true); gestureEvent.gesture = data; data.target.dispatchEvent(gestureEvent); } extend(Hammer, { INPUT_START: INPUT_START, INPUT_MOVE: INPUT_MOVE, INPUT_END: INPUT_END, INPUT_CANCEL: INPUT_CANCEL, STATE_POSSIBLE: STATE_POSSIBLE, STATE_BEGAN: STATE_BEGAN, STATE_CHANGED: STATE_CHANGED, STATE_ENDED: STATE_ENDED, STATE_RECOGNIZED: STATE_RECOGNIZED, STATE_CANCELLED: STATE_CANCELLED, STATE_FAILED: STATE_FAILED, DIRECTION_NONE: DIRECTION_NONE, DIRECTION_LEFT: DIRECTION_LEFT, DIRECTION_RIGHT: DIRECTION_RIGHT, DIRECTION_UP: DIRECTION_UP, DIRECTION_DOWN: DIRECTION_DOWN, DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, DIRECTION_VERTICAL: DIRECTION_VERTICAL, DIRECTION_ALL: DIRECTION_ALL, Manager: Manager, Input: Input, TouchAction: TouchAction, TouchInput: TouchInput, MouseInput: MouseInput, PointerEventInput: PointerEventInput, TouchMouseInput: TouchMouseInput, SingleTouchInput: SingleTouchInput, Recognizer: Recognizer, AttrRecognizer: AttrRecognizer, Tap: TapRecognizer, Pan: PanRecognizer, Swipe: SwipeRecognizer, Pinch: PinchRecognizer, Rotate: RotateRecognizer, Press: PressRecognizer, on: addEventListeners, off: removeEventListeners, each: each, merge: merge, extend: extend, inherit: inherit, bindFn: bindFn, prefixed: prefixed }); if ("function" == TYPE_FUNCTION && __webpack_require__(26)) { !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { return Hammer; }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); } else if (typeof module != 'undefined' && module.exports) { module.exports = Hammer; } else { window[exportName] = Hammer; } })(window, document, 'Hammer'); /***/ }, /* 26 */ /***/ function(module, exports) { /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; /* WEBPACK VAR INJECTION */}.call(exports, {})) /***/ }, /* 27 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var hammerUtil = __webpack_require__(28); var moment = __webpack_require__(2); var Component = __webpack_require__(21); var DateUtil = __webpack_require__(29); /** * @constructor Range * A Range controls a numeric range with a start and end value. * The Range adjusts the range based on mouse events or programmatic changes, * and triggers events when the range is changing or has been changed. * @param {{dom: Object, domProps: Object, emitter: Emitter}} body * @param {Object} [options] See description at Range.setOptions */ function Range(body, options) { var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); this.start = now.clone().add(-3, 'days').valueOf(); // Number this.end = now.clone().add(4, 'days').valueOf(); // Number this.body = body; this.deltaDifference = 0; this.scaleOffset = 0; this.startToFront = false; this.endToFront = true; // default options this.defaultOptions = { start: null, end: null, direction: 'horizontal', // 'horizontal' or 'vertical' moveable: true, zoomable: true, min: null, max: null, zoomMin: 10, // milliseconds zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds }; this.options = util.extend({}, this.defaultOptions); this.props = { touch: {} }; this.animationTimer = null; // drag listeners for dragging this.body.emitter.on('panstart', this._onDragStart.bind(this)); this.body.emitter.on('panmove', this._onDrag.bind(this)); this.body.emitter.on('panend', this._onDragEnd.bind(this)); // mouse wheel for zooming this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); // pinch to zoom this.body.emitter.on('touch', this._onTouch.bind(this)); this.body.emitter.on('pinch', this._onPinch.bind(this)); this.setOptions(options); } Range.prototype = new Component(); /** * Set options for the range controller * @param {Object} options Available options: * {Number | Date | String} start Start date for the range * {Number | Date | String} end End date for the range * {Number} min Minimum value for start * {Number} max Maximum value for end * {Number} zoomMin Set a minimum value for * (end - start). * {Number} zoomMax Set a maximum value for * (end - start). * {Boolean} moveable Enable moving of the range * by dragging. True by default * {Boolean} zoomable Enable zooming of the range * by pinching/scrolling. True by default */ Range.prototype.setOptions = function (options) { if (options) { // copy the options that we know var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates']; util.selectiveExtend(fields, this.options, options); if ('start' in options || 'end' in options) { // apply a new range. both start and end are optional this.setRange(options.start, options.end); } } }; /** * Test whether direction has a valid value * @param {String} direction 'horizontal' or 'vertical' */ function validateDirection(direction) { if (direction != 'horizontal' && direction != 'vertical') { throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); } } /** * Set a new start and end range * @param {Date | Number | String} [start] * @param {Date | Number | String} [end] * @param {boolean | {duration: number, easingFunction: string}} [animation=false] * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. * @param {Boolean} [byUser=false] * */ Range.prototype.setRange = function (start, end, animation, byUser) { if (byUser !== true) { byUser = false; } var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; this._cancelAnimation(); if (animation) { // true or an Object var me = this; var initStart = this.start; var initEnd = this.end; var duration = typeof animation === 'object' && 'duration' in animation ? animation.duration : 500; var easingName = typeof animation === 'object' && 'easingFunction' in animation ? animation.easingFunction : 'easeInOutQuad'; var easingFunction = util.easingFunctions[easingName]; if (!easingFunction) { throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' + 'Choose from: ' + Object.keys(util.easingFunctions).join(', ')); } var initTime = new Date().valueOf(); var anyChanged = false; var next = function next() { if (!me.props.touch.dragging) { var now = new Date().valueOf(); var time = now - initTime; var ease = easingFunction(time / duration); var done = time > duration; var s = done || finalStart === null ? finalStart : initStart + (finalStart - initStart) * ease; var e = done || finalEnd === null ? finalEnd : initEnd + (finalEnd - initEnd) * ease; changed = me._applyRange(s, e); DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); anyChanged = anyChanged || changed; if (changed) { me.body.emitter.emit('rangechange', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); } if (done) { if (anyChanged) { me.body.emitter.emit('rangechanged', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); } } else { // animate with as high as possible frame rate, leave 20 ms in between // each to prevent the browser from blocking me.animationTimer = setTimeout(next, 20); } } }; return next(); } else { var changed = this._applyRange(finalStart, finalEnd); DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); if (changed) { var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; this.body.emitter.emit('rangechange', params); this.body.emitter.emit('rangechanged', params); } } }; /** * Stop an animation * @private */ Range.prototype._cancelAnimation = function () { if (this.animationTimer) { clearTimeout(this.animationTimer); this.animationTimer = null; } }; /** * Set a new start and end range. This method is the same as setRange, but * does not trigger a range change and range changed event, and it returns * true when the range is changed * @param {Number} [start] * @param {Number} [end] * @return {Boolean} changed * @private */ Range.prototype._applyRange = function (start, end) { var newStart = start != null ? util.convert(start, 'Date').valueOf() : this.start, newEnd = end != null ? util.convert(end, 'Date').valueOf() : this.end, max = this.options.max != null ? util.convert(this.options.max, 'Date').valueOf() : null, min = this.options.min != null ? util.convert(this.options.min, 'Date').valueOf() : null, diff; // check for valid number if (isNaN(newStart) || newStart === null) { throw new Error('Invalid start "' + start + '"'); } if (isNaN(newEnd) || newEnd === null) { throw new Error('Invalid end "' + end + '"'); } // prevent start < end if (newEnd < newStart) { newEnd = newStart; } // prevent start < min if (min !== null) { if (newStart < min) { diff = min - newStart; newStart += diff; newEnd += diff; // prevent end > max if (max != null) { if (newEnd > max) { newEnd = max; } } } } // prevent end > max if (max !== null) { if (newEnd > max) { diff = newEnd - max; newStart -= diff; newEnd -= diff; // prevent start < min if (min != null) { if (newStart < min) { newStart = min; } } } } // prevent (end-start) < zoomMin if (this.options.zoomMin !== null) { var zoomMin = parseFloat(this.options.zoomMin); if (zoomMin < 0) { zoomMin = 0; } if (newEnd - newStart < zoomMin) { if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { // ignore this action, we are already zoomed to the minimum newStart = this.start; newEnd = this.end; } else { // zoom to the minimum diff = zoomMin - (newEnd - newStart); newStart -= diff / 2; newEnd += diff / 2; } } } // prevent (end-start) > zoomMax if (this.options.zoomMax !== null) { var zoomMax = parseFloat(this.options.zoomMax); if (zoomMax < 0) { zoomMax = 0; } if (newEnd - newStart > zoomMax) { if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { // ignore this action, we are already zoomed to the maximum newStart = this.start; newEnd = this.end; } else { // zoom to the maximum diff = newEnd - newStart - zoomMax; newStart += diff / 2; newEnd -= diff / 2; } } } var changed = this.start != newStart || this.end != newEnd; // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { this.body.emitter.emit('checkRangedItems'); } this.start = newStart; this.end = newEnd; return changed; }; /** * Retrieve the current range. * @return {Object} An object with start and end properties */ Range.prototype.getRange = function () { return { start: this.start, end: this.end }; }; /** * Calculate the conversion offset and scale for current range, based on * the provided width * @param {Number} width * @returns {{offset: number, scale: number}} conversion */ Range.prototype.conversion = function (width, totalHidden) { return Range.conversion(this.start, this.end, width, totalHidden); }; /** * Static method to calculate the conversion offset and scale for a range, * based on the provided start, end, and width * @param {Number} start * @param {Number} end * @param {Number} width * @returns {{offset: number, scale: number}} conversion */ Range.conversion = function (start, end, width, totalHidden) { if (totalHidden === undefined) { totalHidden = 0; } if (width != 0 && end - start != 0) { return { offset: start, scale: width / (end - start - totalHidden) }; } else { return { offset: 0, scale: 1 }; } }; /** * Start dragging horizontally or vertically * @param {Event} event * @private */ Range.prototype._onDragStart = function (event) { this.deltaDifference = 0; this.previousDelta = 0; // only allow dragging when configured as movable if (!this.options.moveable) return; // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (!this.props.touch.allowDragging) return; this.props.touch.start = this.start; this.props.touch.end = this.end; this.props.touch.dragging = true; if (this.body.dom.root) { this.body.dom.root.style.cursor = 'move'; } }; /** * Perform dragging operation * @param {Event} event * @private */ Range.prototype._onDrag = function (event) { // only allow dragging when configured as movable if (!this.options.moveable) return; // TODO: this may be redundant in hammerjs2 // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (!this.props.touch.allowDragging) return; var direction = this.options.direction; validateDirection(direction); var delta = direction == 'horizontal' ? event.deltaX : event.deltaY; delta -= this.deltaDifference; var interval = this.props.touch.end - this.props.touch.start; // normalize dragging speed if cutout is in between. var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); interval -= duration; var width = direction == 'horizontal' ? this.body.domProps.center.width : this.body.domProps.center.height; var diffRange = -delta / width * interval; var newStart = this.props.touch.start + diffRange; var newEnd = this.props.touch.end + diffRange; // snapping times away from hidden zones var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); if (safeStart != newStart || safeEnd != newEnd) { this.deltaDifference += delta; this.props.touch.start = safeStart; this.props.touch.end = safeEnd; this._onDrag(event); return; } this.previousDelta = delta; this._applyRange(newStart, newEnd); // fire a rangechange event this.body.emitter.emit('rangechange', { start: new Date(this.start), end: new Date(this.end), byUser: true }); }; /** * Stop dragging operation * @param {event} event * @private */ Range.prototype._onDragEnd = function (event) { // only allow dragging when configured as movable if (!this.options.moveable) return; // TODO: this may be redundant in hammerjs2 // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (!this.props.touch.allowDragging) return; this.props.touch.dragging = false; if (this.body.dom.root) { this.body.dom.root.style.cursor = 'auto'; } // fire a rangechanged event this.body.emitter.emit('rangechanged', { start: new Date(this.start), end: new Date(this.end), byUser: true }); }; /** * Event handler for mouse wheel event, used to zoom * Code from http://adomas.org/javascript-mouse-wheel/ * @param {Event} event * @private */ Range.prototype._onMouseWheel = function (event) { // only allow zooming when configured as zoomable and moveable if (!(this.options.zoomable && this.options.moveable)) return; // retrieve delta var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ delta = event.wheelDelta / 120; } else if (event.detail) { /* Mozilla case. */ // In Mozilla, sign of delta is different than in IE. // Also, delta is multiple of 3. delta = -event.detail / 3; } // If delta is nonzero, handle it. // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. if (delta) { // perform the zoom action. Delta is normally 1 or -1 // adjust a negative delta such that zooming in with delta 0.1 // equals zooming out with a delta -0.1 var scale; if (delta < 0) { scale = 1 - delta / 5; } else { scale = 1 / (1 + delta / 5); } // calculate center, the date to zoom around var pointer = getPointer({ x: event.clientX, y: event.clientY }, this.body.dom.center); var pointerDate = this._pointerToDate(pointer); this.zoom(scale, pointerDate, delta); } // Prevent default actions caused by mouse wheel // (else the page and timeline both zoom and scroll) event.preventDefault(); }; /** * Start of a touch gesture * @private */ Range.prototype._onTouch = function (event) { this.props.touch.start = this.start; this.props.touch.end = this.end; this.props.touch.allowDragging = true; this.props.touch.center = null; this.scaleOffset = 0; this.deltaDifference = 0; }; /** * Handle pinch event * @param {Event} event * @private */ Range.prototype._onPinch = function (event) { // only allow zooming when configured as zoomable and moveable if (!(this.options.zoomable && this.options.moveable)) return; this.props.touch.allowDragging = false; if (!this.props.touch.center) { this.props.touch.center = getPointer(event.center, this.body.dom.center); } var scale = 1 / (event.scale + this.scaleOffset); var centerDate = this._pointerToDate(this.props.touch.center); var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; // calculate new start and end var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; // snapping times away from hidden zones this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); if (safeStart != newStart || safeEnd != newEnd) { this.props.touch.start = safeStart; this.props.touch.end = safeEnd; this.scaleOffset = 1 - event.scale; newStart = safeStart; newEnd = safeEnd; } this.setRange(newStart, newEnd, false, true); this.startToFront = false; // revert to default this.endToFront = true; // revert to default }; /** * Helper function to calculate the center date for zooming * @param {{x: Number, y: Number}} pointer * @return {number} date * @private */ Range.prototype._pointerToDate = function (pointer) { var conversion; var direction = this.options.direction; validateDirection(direction); if (direction == 'horizontal') { return this.body.util.toTime(pointer.x).valueOf(); } else { var height = this.body.domProps.center.height; conversion = this.conversion(height); return pointer.y / conversion.scale + conversion.offset; } }; /** * Get the pointer location relative to the location of the dom element * @param {{x: Number, y: Number}} touch * @param {Element} element HTML DOM element * @return {{x: Number, y: Number}} pointer * @private */ function getPointer(touch, element) { return { x: touch.x - util.getAbsoluteLeft(element), y: touch.y - util.getAbsoluteTop(element) }; } /** * Zoom the range the given scale in or out. Start and end date will * be adjusted, and the timeline will be redrawn. You can optionally give a * date around which to zoom. * For example, try scale = 0.9 or 1.1 * @param {Number} scale Scaling factor. Values above 1 will zoom out, * values below 1 will zoom in. * @param {Number} [center] Value representing a date around which will * be zoomed. */ Range.prototype.zoom = function (scale, center, delta) { // if centerDate is not provided, take it half between start Date and end Date if (center == null) { center = (this.start + this.end) / 2; } var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; // calculate new start and end var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; // snapping times away from hidden zones this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); if (safeStart != newStart || safeEnd != newEnd) { newStart = safeStart; newEnd = safeEnd; } this.setRange(newStart, newEnd, false, true); this.startToFront = false; // revert to default this.endToFront = true; // revert to default }; /** * Move the range with a given delta to the left or right. Start and end * value will be adjusted. For example, try delta = 0.1 or -0.1 * @param {Number} delta Moving amount. Positive value will move right, * negative value will move left */ Range.prototype.move = function (delta) { // zoom start Date and end Date relative to the centerDate var diff = this.end - this.start; // apply new values var newStart = this.start + diff * delta; var newEnd = this.end + diff * delta; // TODO: reckon with min and max range this.start = newStart; this.end = newEnd; }; /** * Move the range to a new center point * @param {Number} moveTo New center point of the range */ Range.prototype.moveTo = function (moveTo) { var center = (this.start + this.end) / 2; var diff = center - moveTo; // calculate new start and end var newStart = this.start - diff; var newEnd = this.end - diff; this.setRange(newStart, newEnd); }; module.exports = Range; /***/ }, /* 28 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Hammer = __webpack_require__(23); /** * Register a touch event, taking place before a gesture * @param {Hammer} hammer A hammer instance * @param {function} callback Callback, called as callback(event) */ exports.onTouch = function (hammer, callback) { callback.inputHandler = function (event) { if (event.isFirst && !isTouching) { callback(event); isTouching = true; setTimeout(function () { isTouching = false; }, 0); } }; hammer.on('hammer.input', callback.inputHandler); }; // isTouching is true while a touch action is being emitted // this is a hack to prevent `touch` from being fired twice var isTouching = false; /** * Register a release event, taking place after a gesture * @param {Hammer} hammer A hammer instance * @param {function} callback Callback, called as callback(event) */ exports.onRelease = function (hammer, callback) { callback.inputHandler = function (event) { if (event.isFinal && !isReleasing) { callback(event); isReleasing = true; setTimeout(function () { isReleasing = false; }, 0); } }; return hammer.on('hammer.input', callback.inputHandler); }; // isReleasing is true while a release action is being emitted // this is a hack to prevent `release` from being fired twice var isReleasing = false; /** * Unregister a touch event, taking place before a gesture * @param {Hammer} hammer A hammer instance * @param {function} callback Callback, called as callback(event) */ exports.offTouch = function (hammer, callback) { hammer.off('hammer.input', callback.inputHandler); }; /** * Unregister a release event, taking place before a gesture * @param {Hammer} hammer A hammer instance * @param {function} callback Callback, called as callback(event) */ exports.offRelease = exports.offTouch; /***/ }, /* 29 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var moment = __webpack_require__(2); /** * used in Core to convert the options into a volatile variable * * @param Core */ exports.convertHiddenOptions = function (body, hiddenDates) { body.hiddenDates = []; if (hiddenDates) { if (Array.isArray(hiddenDates) == true) { for (var i = 0; i < hiddenDates.length; i++) { if (hiddenDates[i].repeat === undefined) { var dateItem = {}; dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); body.hiddenDates.push(dateItem); } } body.hiddenDates.sort(function (a, b) { return a.start - b.start; }); // sort by start time } } }; /** * create new entrees for the repeating hidden dates * @param body * @param hiddenDates */ exports.updateHiddenDates = function (body, hiddenDates) { if (hiddenDates && body.domProps.centerContainer.width !== undefined) { exports.convertHiddenOptions(body, hiddenDates); var start = moment(body.range.start); var end = moment(body.range.end); var totalRange = body.range.end - body.range.start; var pixelTime = totalRange / body.domProps.centerContainer.width; for (var i = 0; i < hiddenDates.length; i++) { if (hiddenDates[i].repeat !== undefined) { var startDate = moment(hiddenDates[i].start); var endDate = moment(hiddenDates[i].end); if (startDate._d == "Invalid Date") { throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); } if (endDate._d == "Invalid Date") { throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); } var duration = endDate - startDate; if (duration >= 4 * pixelTime) { var offset = 0; var runUntil = end.clone(); switch (hiddenDates[i].repeat) { case "daily": // case of time if (startDate.day() != endDate.day()) { offset = 1; } startDate.dayOfYear(start.dayOfYear()); startDate.year(start.year()); startDate.subtract(7, "days"); endDate.dayOfYear(start.dayOfYear()); endDate.year(start.year()); endDate.subtract(7 - offset, "days"); runUntil.add(1, "weeks"); break; case "weekly": var dayOffset = endDate.diff(startDate, "days"); var day = startDate.day(); // set the start date to the range.start startDate.date(start.date()); startDate.month(start.month()); startDate.year(start.year()); endDate = startDate.clone(); // force startDate.day(day); endDate.day(day); endDate.add(dayOffset, "days"); startDate.subtract(1, "weeks"); endDate.subtract(1, "weeks"); runUntil.add(1, "weeks"); break; case "monthly": if (startDate.month() != endDate.month()) { offset = 1; } startDate.month(start.month()); startDate.year(start.year()); startDate.subtract(1, "months"); endDate.month(start.month()); endDate.year(start.year()); endDate.subtract(1, "months"); endDate.add(offset, "months"); runUntil.add(1, "months"); break; case "yearly": if (startDate.year() != endDate.year()) { offset = 1; } startDate.year(start.year()); startDate.subtract(1, "years"); endDate.year(start.year()); endDate.subtract(1, "years"); endDate.add(offset, "years"); runUntil.add(1, "years"); break; default: console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); return; } while (startDate < runUntil) { body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); switch (hiddenDates[i].repeat) { case "daily": startDate.add(1, "days"); endDate.add(1, "days"); break; case "weekly": startDate.add(1, "weeks"); endDate.add(1, "weeks"); break; case "monthly": startDate.add(1, "months"); endDate.add(1, "months"); break; case "yearly": startDate.add(1, "y"); endDate.add(1, "y"); break; default: console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); return; } } body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); } } } // remove duplicates, merge where possible exports.removeDuplicates(body); // ensure the new positions are not on hidden dates var startHidden = exports.isHidden(body.range.start, body.hiddenDates); var endHidden = exports.isHidden(body.range.end, body.hiddenDates); var rangeStart = body.range.start; var rangeEnd = body.range.end; if (startHidden.hidden == true) { rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; } if (endHidden.hidden == true) { rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; } if (startHidden.hidden == true || endHidden.hidden == true) { body.range._applyRange(rangeStart, rangeEnd); } } }; /** * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. * Scales with N^2 * @param body */ exports.removeDuplicates = function (body) { var hiddenDates = body.hiddenDates; var safeDates = []; for (var i = 0; i < hiddenDates.length; i++) { for (var j = 0; j < hiddenDates.length; j++) { if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { // j inside i if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { hiddenDates[j].remove = true; } // j start inside i else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { hiddenDates[i].end = hiddenDates[j].end; hiddenDates[j].remove = true; } // j end inside i else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { hiddenDates[i].start = hiddenDates[j].start; hiddenDates[j].remove = true; } } } } for (var i = 0; i < hiddenDates.length; i++) { if (hiddenDates[i].remove !== true) { safeDates.push(hiddenDates[i]); } } body.hiddenDates = safeDates; body.hiddenDates.sort(function (a, b) { return a.start - b.start; }); // sort by start time }; exports.printDates = function (dates) { for (var i = 0; i < dates.length; i++) { console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); } }; /** * Used in TimeStep to avoid the hidden times. * @param timeStep * @param previousTime */ exports.stepOverHiddenDates = function (timeStep, previousTime) { var stepInHidden = false; var currentValue = timeStep.current.valueOf(); for (var i = 0; i < timeStep.hiddenDates.length; i++) { var startDate = timeStep.hiddenDates[i].start; var endDate = timeStep.hiddenDates[i].end; if (currentValue >= startDate && currentValue < endDate) { stepInHidden = true; break; } } if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { var prevValue = moment(previousTime); var newValue = moment(endDate); //check if the next step should be major if (prevValue.year() != newValue.year()) { timeStep.switchedYear = true; } else if (prevValue.month() != newValue.month()) { timeStep.switchedMonth = true; } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { timeStep.switchedDay = true; } timeStep.current = newValue.toDate(); } }; ///** // * Used in TimeStep to avoid the hidden times. // * @param timeStep // * @param previousTime // */ //exports.checkFirstStep = function(timeStep) { // var stepInHidden = false; // var currentValue = timeStep.current.valueOf(); // for (var i = 0; i < timeStep.hiddenDates.length; i++) { // var startDate = timeStep.hiddenDates[i].start; // var endDate = timeStep.hiddenDates[i].end; // if (currentValue >= startDate && currentValue < endDate) { // stepInHidden = true; // break; // } // } // // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { // var newValue = moment(endDate); // timeStep.current = newValue.toDate(); // } //}; /** * replaces the Core toScreen methods * @param Core * @param time * @param width * @returns {number} */ exports.toScreen = function (Core, time, width) { if (Core.body.hiddenDates.length == 0) { var conversion = Core.range.conversion(width); return (time.valueOf() - conversion.offset) * conversion.scale; } else { var hidden = exports.isHidden(time, Core.body.hiddenDates); if (hidden.hidden == true) { time = hidden.startDate; } var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); var conversion = Core.range.conversion(width, duration); return (time.valueOf() - conversion.offset) * conversion.scale; } }; /** * Replaces the core toTime methods * @param body * @param range * @param x * @param width * @returns {Date} */ exports.toTime = function (Core, x, width) { if (Core.body.hiddenDates.length == 0) { var conversion = Core.range.conversion(width); return new Date(x / conversion.scale + conversion.offset); } else { var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); var totalDuration = Core.range.end - Core.range.start - hiddenDuration; var partialDuration = totalDuration * x / width; var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); return newTime; } }; /** * Support function * * @param hiddenDates * @param range * @returns {number} */ exports.getHiddenDurationBetween = function (hiddenDates, start, end) { var duration = 0; for (var i = 0; i < hiddenDates.length; i++) { var startDate = hiddenDates[i].start; var endDate = hiddenDates[i].end; // if time after the cutout, and the if (startDate >= start && endDate < end) { duration += endDate - startDate; } } return duration; }; /** * Support function * @param hiddenDates * @param range * @param time * @returns {{duration: number, time: *, offset: number}} */ exports.correctTimeForHidden = function (hiddenDates, range, time) { time = moment(time).toDate().valueOf(); time -= exports.getHiddenDurationBefore(hiddenDates, range, time); return time; }; exports.getHiddenDurationBefore = function (hiddenDates, range, time) { var timeOffset = 0; time = moment(time).toDate().valueOf(); for (var i = 0; i < hiddenDates.length; i++) { var startDate = hiddenDates[i].start; var endDate = hiddenDates[i].end; // if time after the cutout, and the if (startDate >= range.start && endDate < range.end) { if (time >= endDate) { timeOffset += endDate - startDate; } } } return timeOffset; }; /** * sum the duration from start to finish, including the hidden duration, * until the required amount has been reached, return the accumulated hidden duration * @param hiddenDates * @param range * @param time * @returns {{duration: number, time: *, offset: number}} */ exports.getAccumulatedHiddenDuration = function (hiddenDates, range, requiredDuration) { var hiddenDuration = 0; var duration = 0; var previousPoint = range.start; //exports.printDates(hiddenDates) for (var i = 0; i < hiddenDates.length; i++) { var startDate = hiddenDates[i].start; var endDate = hiddenDates[i].end; // if time after the cutout, and the if (startDate >= range.start && endDate < range.end) { duration += startDate - previousPoint; previousPoint = endDate; if (duration >= requiredDuration) { break; } else { hiddenDuration += endDate - startDate; } } } return hiddenDuration; }; /** * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true * @param hiddenDates * @param time * @param direction * @param correctionEnabled * @returns {*} */ exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { var isHidden = exports.isHidden(time, hiddenDates); if (isHidden.hidden == true) { if (direction < 0) { if (correctionEnabled == true) { return isHidden.startDate - (isHidden.endDate - time) - 1; } else { return isHidden.startDate - 1; } } else { if (correctionEnabled == true) { return isHidden.endDate + (time - isHidden.startDate) + 1; } else { return isHidden.endDate + 1; } } } else { return time; } }; /** * Check if a time is hidden * * @param time * @param hiddenDates * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ exports.isHidden = function (time, hiddenDates) { for (var i = 0; i < hiddenDates.length; i++) { var startDate = hiddenDates[i].start; var endDate = hiddenDates[i].end; if (time >= startDate && time < endDate) { // if the start is entering a hidden zone return { hidden: true, startDate: startDate, endDate: endDate }; break; } } return { hidden: false, startDate: startDate, endDate: endDate }; }; /***/ }, /* 30 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Emitter = __webpack_require__(13); var Hammer = __webpack_require__(23); var hammerUtil = __webpack_require__(28); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var Range = __webpack_require__(27); var ItemSet = __webpack_require__(31); var TimeAxis = __webpack_require__(41); var Activator = __webpack_require__(42); var DateUtil = __webpack_require__(29); var CustomTime = __webpack_require__(44); /** * Create a timeline visualization * @constructor */ function Core() {} // turn Core into an event emitter Emitter(Core.prototype); /** * Create the main DOM for the Core: a root panel containing left, right, * top, bottom, content, and background panel. * @param {Element} container The container element where the Core will * be attached. * @protected */ Core.prototype._create = function (container) { this.dom = {}; this.dom.container = container; this.dom.root = document.createElement('div'); this.dom.background = document.createElement('div'); this.dom.backgroundVertical = document.createElement('div'); this.dom.backgroundHorizontal = document.createElement('div'); this.dom.centerContainer = document.createElement('div'); this.dom.leftContainer = document.createElement('div'); this.dom.rightContainer = document.createElement('div'); this.dom.center = document.createElement('div'); this.dom.left = document.createElement('div'); this.dom.right = document.createElement('div'); this.dom.top = document.createElement('div'); this.dom.bottom = document.createElement('div'); this.dom.shadowTop = document.createElement('div'); this.dom.shadowBottom = document.createElement('div'); this.dom.shadowTopLeft = document.createElement('div'); this.dom.shadowBottomLeft = document.createElement('div'); this.dom.shadowTopRight = document.createElement('div'); this.dom.shadowBottomRight = document.createElement('div'); this.dom.root.className = 'vis-timeline'; this.dom.background.className = 'vis-panel vis-background'; this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical'; this.dom.backgroundHorizontal.className = 'vis-panel vis-background vis-horizontal'; this.dom.centerContainer.className = 'vis-panel vis-center'; this.dom.leftContainer.className = 'vis-panel vis-left'; this.dom.rightContainer.className = 'vis-panel vis-right'; this.dom.top.className = 'vis-panel vis-top'; this.dom.bottom.className = 'vis-panel vis-bottom'; this.dom.left.className = 'vis-content'; this.dom.center.className = 'vis-content'; this.dom.right.className = 'vis-content'; this.dom.shadowTop.className = 'vis-shadow vis-top'; this.dom.shadowBottom.className = 'vis-shadow vis-bottom'; this.dom.shadowTopLeft.className = 'vis-shadow vis-top'; this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom'; this.dom.shadowTopRight.className = 'vis-shadow vis-top'; this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom'; this.dom.root.appendChild(this.dom.background); this.dom.root.appendChild(this.dom.backgroundVertical); this.dom.root.appendChild(this.dom.backgroundHorizontal); this.dom.root.appendChild(this.dom.centerContainer); this.dom.root.appendChild(this.dom.leftContainer); this.dom.root.appendChild(this.dom.rightContainer); this.dom.root.appendChild(this.dom.top); this.dom.root.appendChild(this.dom.bottom); this.dom.centerContainer.appendChild(this.dom.center); this.dom.leftContainer.appendChild(this.dom.left); this.dom.rightContainer.appendChild(this.dom.right); this.dom.centerContainer.appendChild(this.dom.shadowTop); this.dom.centerContainer.appendChild(this.dom.shadowBottom); this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); this.dom.rightContainer.appendChild(this.dom.shadowTopRight); this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); this.on('rangechange', this.redraw.bind(this)); this.on('touch', this._onTouch.bind(this)); this.on('pan', this._onDrag.bind(this)); var me = this; this.on('change', function (properties) { if (properties && properties.queue == true) { // redraw once on next tick if (!me._redrawTimer) { me._redrawTimer = setTimeout(function () { me._redrawTimer = null; me._redraw(); }, 0); } } else { // redraw immediately me._redraw(); } }); // create event listeners for all interesting events, these events will be // emitted via emitter this.hammer = new Hammer(this.dom.root); this.hammer.get('pinch').set({ enable: true }); this.listeners = {}; var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend' // TODO: cleanup //'touch', 'pinch', //'tap', 'doubletap', 'hold', //'dragstart', 'drag', 'dragend', //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox ]; events.forEach(function (type) { var listener = function listener(event) { if (me.isActive()) { me.emit(type, event); } }; me.hammer.on(type, listener); me.listeners[type] = listener; }); // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) hammerUtil.onTouch(this.hammer, (function (event) { me.emit('touch', event); }).bind(this)); // emulate a release event (emitted after a pan, pinch, tap, or press) hammerUtil.onRelease(this.hammer, (function (event) { me.emit('release', event); }).bind(this)); function onMouseWheel(event) { if (me.isActive()) { me.emit('mousewheel', event); } } this.dom.root.addEventListener('mousewheel', onMouseWheel); this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); // size properties of each of the panels this.props = { root: {}, background: {}, centerContainer: {}, leftContainer: {}, rightContainer: {}, center: {}, left: {}, right: {}, top: {}, bottom: {}, border: {}, scrollTop: 0, scrollTopMin: 0 }; this.customTimes = []; // store state information needed for touch events this.touch = {}; this.redrawCount = 0; // attach the root panel to the provided container if (!container) throw new Error('No container provided'); container.appendChild(this.dom.root); }; /** * Set options. Options will be passed to all components loaded in the Timeline. * @param {Object} [options] * {String} orientation * Vertical orientation for the Timeline, * can be 'bottom' (default) or 'top'. * {String | Number} width * Width for the timeline, a number in pixels or * a css string like '1000px' or '75%'. '100%' by default. * {String | Number} height * Fixed height for the Timeline, a number in pixels or * a css string like '400px' or '75%'. If undefined, * The Timeline will automatically size such that * its contents fit. * {String | Number} minHeight * Minimum height for the Timeline, a number in pixels or * a css string like '400px' or '75%'. * {String | Number} maxHeight * Maximum height for the Timeline, a number in pixels or * a css string like '400px' or '75%'. * {Number | Date | String} start * Start date for the visible window * {Number | Date | String} end * End date for the visible window */ Core.prototype.setOptions = function (options) { if (options) { // copy the known options var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates']; util.selectiveExtend(fields, this.options, options); if ('orientation' in options) { if (typeof options.orientation === 'string') { this.options.orientation = { item: options.orientation, axis: options.orientation }; } else if (typeof options.orientation === 'object') { if ('item' in options.orientation) { this.options.orientation.item = options.orientation.item; } if ('axis' in options.orientation) { this.options.orientation.axis = options.orientation.axis; } } } if (this.options.orientation.axis === 'both') { if (!this.timeAxis2) { var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body); timeAxis2.setOptions = function (options) { var _options = options ? util.extend({}, options) : {}; _options.orientation = 'top'; // override the orientation option, always top TimeAxis.prototype.setOptions.call(timeAxis2, _options); }; this.components.push(timeAxis2); } } else { if (this.timeAxis2) { var index = this.components.indexOf(this.timeAxis2); if (index !== -1) { this.components.splice(index, 1); } this.timeAxis2.destroy(); this.timeAxis2 = null; } } if ('hiddenDates' in this.options) { DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); } if ('clickToUse' in options) { if (options.clickToUse) { if (!this.activator) { this.activator = new Activator(this.dom.root); } } else { if (this.activator) { this.activator.destroy(); delete this.activator; } } } if ('showCustomTime' in options) { throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])'); } // enable/disable autoResize this._initAutoResize(); } // propagate options to all components this.components.forEach(function (component) { return component.setOptions(options); }); // enable/disable configure if ('configure' in options) { if (!this.configurator) { this.configurator = this._createConfigurator(); } this.configurator.setOptions(options.configure); // collect the settings of all components, and pass them to the configuration system var appliedOptions = util.deepExtend({}, this.options); this.components.forEach(function (component) { util.deepExtend(appliedOptions, component.options); }); this.configurator.setModuleOptions({ global: appliedOptions }); } // redraw everything this._redraw(); }; /** * Returns true when the Timeline is active. * @returns {boolean} */ Core.prototype.isActive = function () { return !this.activator || this.activator.active; }; /** * Destroy the Core, clean up all DOM elements and event listeners. */ Core.prototype.destroy = function () { // unbind datasets this.setItems(null); this.setGroups(null); // remove all event listeners this.off(); // stop checking for changed size this._stopAutoResize(); // remove from DOM if (this.dom.root.parentNode) { this.dom.root.parentNode.removeChild(this.dom.root); } this.dom = null; // remove Activator if (this.activator) { this.activator.destroy(); delete this.activator; } // cleanup hammer touch events for (var event in this.listeners) { if (this.listeners.hasOwnProperty(event)) { delete this.listeners[event]; } } this.listeners = null; this.hammer = null; // give all components the opportunity to cleanup this.components.forEach(function (component) { return component.destroy(); }); this.body = null; }; /** * Set a custom time bar * @param {Date} time * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. */ Core.prototype.setCustomTime = function (time, id) { var customTimes = this.customTimes.filter(function (component) { return id === component.options.id; }); if (customTimes.length === 0) { throw new Error('No custom time bar found with id ' + JSON.stringify(id)); } if (customTimes.length > 0) { customTimes[0].setCustomTime(time); } }; /** * Retrieve the current custom time. * @param {number} [id=undefined] Id of the custom time bar. * @return {Date | undefined} customTime */ Core.prototype.getCustomTime = function (id) { var customTimes = this.customTimes.filter(function (component) { return component.options.id === id; }); if (customTimes.length === 0) { throw new Error('No custom time bar found with id ' + JSON.stringify(id)); } return customTimes[0].getCustomTime(); }; /** * Add custom vertical bar * @param {Date | String | Number} [time] A Date, unix timestamp, or * ISO date string. Time point where * the new bar should be placed. * If not provided, `new Date()` will * be used. * @param {Number | String} [id=undefined] Id of the new bar. Optional * @return {Number | String} Returns the id of the new bar */ Core.prototype.addCustomTime = function (time, id) { var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); var exists = this.customTimes.some(function (customTime) { return customTime.options.id === id; }); if (exists) { throw new Error('A custom time with id ' + JSON.stringify(id) + ' already exists'); } var customTime = new CustomTime(this.body, { time: timestamp, id: id }); this.customTimes.push(customTime); this.components.push(customTime); this.redraw(); return id; }; /** * Remove previously added custom bar * @param {int} id ID of the custom bar to be removed * @return {boolean} True if the bar exists and is removed, false otherwise */ Core.prototype.removeCustomTime = function (id) { var customTimes = this.customTimes.filter(function (bar) { return bar.options.id === id; }); if (customTimes.length === 0) { throw new Error('No custom time bar found with id ' + JSON.stringify(id)); } customTimes.forEach((function (customTime) { this.customTimes.splice(this.customTimes.indexOf(customTime), 1); this.components.splice(this.components.indexOf(customTime), 1); customTime.destroy(); }).bind(this)); }; /** * Get the id's of the currently visible items. * @returns {Array} The ids of the visible items */ Core.prototype.getVisibleItems = function () { return this.itemSet && this.itemSet.getVisibleItems() || []; }; /** * Set Core window such that it fits all items * @param {Object} [options] Available options: * `animation: boolean | {duration: number, easingFunction: string}` * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. */ Core.prototype.fit = function (options) { var range = this.getDataRange(); // skip range set if there is no min and max date if (range.min === null && range.max === null) { return; } // apply a margin of 1% left and right of the data var interval = range.max - range.min; var min = new Date(range.min.valueOf() - interval * 0.01); var max = new Date(range.max.valueOf() + interval * 0.01); var animation = options && options.animation !== undefined ? options.animation : true; this.range.setRange(min, max, animation); }; /** * Calculate the data range of the items start and end dates * @returns {{min: Date | null, max: Date | null}} * @protected */ Core.prototype.getDataRange = function () { // must be implemented by Timeline and Graph2d throw new Error('Cannot invoke abstract method getDataRange'); }; /** * Set the visible window. Both parameters are optional, you can change only * start or only end. Syntax: * * TimeLine.setWindow(start, end) * TimeLine.setWindow(start, end, options) * TimeLine.setWindow(range) * * Where start and end can be a Date, number, or string, and range is an * object with properties start and end. * * @param {Date | Number | String | Object} [start] Start date of visible window * @param {Date | Number | String} [end] End date of visible window * @param {Object} [options] Available options: * `animation: boolean | {duration: number, easingFunction: string}` * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. */ Core.prototype.setWindow = function (start, end, options) { var animation; if (arguments.length == 1) { var range = arguments[0]; animation = range.animation !== undefined ? range.animation : true; this.range.setRange(range.start, range.end, animation); } else { animation = options && options.animation !== undefined ? options.animation : true; this.range.setRange(start, end, animation); } }; /** * Move the window such that given time is centered on screen. * @param {Date | Number | String} time * @param {Object} [options] Available options: * `animation: boolean | {duration: number, easingFunction: string}` * If true (default), the range is animated * smoothly to the new window. An object can be * provided to specify duration and easing function. * Default duration is 500 ms, and default easing * function is 'easeInOutQuad'. */ Core.prototype.moveTo = function (time, options) { var interval = this.range.end - this.range.start; var t = util.convert(time, 'Date').valueOf(); var start = t - interval / 2; var end = t + interval / 2; var animation = options && options.animation !== undefined ? options.animation : true; this.range.setRange(start, end, animation); }; /** * Get the visible window * @return {{start: Date, end: Date}} Visible range */ Core.prototype.getWindow = function () { var range = this.range.getRange(); return { start: new Date(range.start), end: new Date(range.end) }; }; /** * Force a redraw. Can be overridden by implementations of Core */ Core.prototype.redraw = function () { this._redraw(); }; /** * Redraw for internal use. Redraws all components. See also the public * method redraw. * @protected */ Core.prototype._redraw = function () { var resized = false; var options = this.options; var props = this.props; var dom = this.dom; if (!dom) return; // when destroyed DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); // update class names if (options.orientation == 'top') { util.addClassName(dom.root, 'vis-top'); util.removeClassName(dom.root, 'vis-bottom'); } else { util.removeClassName(dom.root, 'vis-top'); util.addClassName(dom.root, 'vis-bottom'); } // update root width and height options dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); dom.root.style.width = util.option.asSize(options.width, ''); // calculate border widths props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; props.border.right = props.border.left; props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; props.border.bottom = props.border.top; var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; // workaround for a bug in IE: the clientWidth of an element with // a height:0px and overflow:hidden is not calculated and always has value 0 if (dom.centerContainer.clientHeight === 0) { props.border.left = props.border.top; props.border.right = props.border.left; } if (dom.root.clientHeight === 0) { borderRootWidth = borderRootHeight; } // calculate the heights. If any of the side panels is empty, we set the height to // minus the border width, such that the border will be invisible props.center.height = dom.center.offsetHeight; props.left.height = dom.left.offsetHeight; props.right.height = dom.right.offsetHeight; props.top.height = dom.top.clientHeight || -props.border.top; props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; // TODO: compensate borders when any of the panels is empty. // apply auto height // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); // calculate heights of the content panels props.root.height = dom.root.offsetHeight; props.background.height = props.root.height - borderRootHeight; var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; props.centerContainer.height = containerHeight; props.leftContainer.height = containerHeight; props.rightContainer.height = props.leftContainer.height; // calculate the widths of the panels props.root.width = dom.root.offsetWidth; props.background.width = props.root.width - borderRootWidth; props.left.width = dom.leftContainer.clientWidth || -props.border.left; props.leftContainer.width = props.left.width; props.right.width = dom.rightContainer.clientWidth || -props.border.right; props.rightContainer.width = props.right.width; var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; props.center.width = centerWidth; props.centerContainer.width = centerWidth; props.top.width = centerWidth; props.bottom.width = centerWidth; // resize the panels dom.background.style.height = props.background.height + 'px'; dom.backgroundVertical.style.height = props.background.height + 'px'; dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; dom.centerContainer.style.height = props.centerContainer.height + 'px'; dom.leftContainer.style.height = props.leftContainer.height + 'px'; dom.rightContainer.style.height = props.rightContainer.height + 'px'; dom.background.style.width = props.background.width + 'px'; dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; dom.backgroundHorizontal.style.width = props.background.width + 'px'; dom.centerContainer.style.width = props.center.width + 'px'; dom.top.style.width = props.top.width + 'px'; dom.bottom.style.width = props.bottom.width + 'px'; // reposition the panels dom.background.style.left = '0'; dom.background.style.top = '0'; dom.backgroundVertical.style.left = props.left.width + props.border.left + 'px'; dom.backgroundVertical.style.top = '0'; dom.backgroundHorizontal.style.left = '0'; dom.backgroundHorizontal.style.top = props.top.height + 'px'; dom.centerContainer.style.left = props.left.width + 'px'; dom.centerContainer.style.top = props.top.height + 'px'; dom.leftContainer.style.left = '0'; dom.leftContainer.style.top = props.top.height + 'px'; dom.rightContainer.style.left = props.left.width + props.center.width + 'px'; dom.rightContainer.style.top = props.top.height + 'px'; dom.top.style.left = props.left.width + 'px'; dom.top.style.top = '0'; dom.bottom.style.left = props.left.width + 'px'; dom.bottom.style.top = props.top.height + props.centerContainer.height + 'px'; // update the scrollTop, feasible range for the offset can be changed // when the height of the Core or of the contents of the center changed this._updateScrollTop(); // reposition the scrollable contents var offset = this.props.scrollTop; if (options.orientation.item != 'top') { offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); } dom.center.style.left = '0'; dom.center.style.top = offset + 'px'; dom.left.style.left = '0'; dom.left.style.top = offset + 'px'; dom.right.style.left = '0'; dom.right.style.top = offset + 'px'; // show shadows when vertical scrolling is available var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; dom.shadowTop.style.visibility = visibilityTop; dom.shadowBottom.style.visibility = visibilityBottom; dom.shadowTopLeft.style.visibility = visibilityTop; dom.shadowBottomLeft.style.visibility = visibilityBottom; dom.shadowTopRight.style.visibility = visibilityTop; dom.shadowBottomRight.style.visibility = visibilityBottom; // redraw all components this.components.forEach(function (component) { resized = component.redraw() || resized; }); if (resized) { // keep repainting until all sizes are settled var MAX_REDRAWS = 3; // maximum number of consecutive redraws if (this.redrawCount < MAX_REDRAWS) { this.redrawCount++; this._redraw(); } else { console.log('WARNING: infinite loop in redraw?'); } this.redrawCount = 0; } }; // TODO: deprecated since version 1.1.0, remove some day Core.prototype.repaint = function () { throw new Error('Function repaint is deprecated. Use redraw instead.'); }; /** * Set a current time. This can be used for example to ensure that a client's * time is synchronized with a shared server time. * Only applicable when option `showCurrentTime` is true. * @param {Date | String | Number} time A Date, unix timestamp, or * ISO date string. */ Core.prototype.setCurrentTime = function (time) { if (!this.currentTime) { throw new Error('Option showCurrentTime must be true'); } this.currentTime.setCurrentTime(time); }; /** * Get the current time. * Only applicable when option `showCurrentTime` is true. * @return {Date} Returns the current time. */ Core.prototype.getCurrentTime = function () { if (!this.currentTime) { throw new Error('Option showCurrentTime must be true'); } return this.currentTime.getCurrentTime(); }; /** * Convert a position on screen (pixels) to a datetime * @param {int} x Position on the screen in pixels * @return {Date} time The datetime the corresponds with given position x * @protected */ // TODO: move this function to Range Core.prototype._toTime = function (x) { return DateUtil.toTime(this, x, this.props.center.width); }; /** * Convert a position on the global screen (pixels) to a datetime * @param {int} x Position on the screen in pixels * @return {Date} time The datetime the corresponds with given position x * @protected */ // TODO: move this function to Range Core.prototype._toGlobalTime = function (x) { return DateUtil.toTime(this, x, this.props.root.width); //var conversion = this.range.conversion(this.props.root.width); //return new Date(x / conversion.scale + conversion.offset); }; /** * Convert a datetime (Date object) into a position on the screen * @param {Date} time A date * @return {int} x The position on the screen in pixels which corresponds * with the given date. * @protected */ // TODO: move this function to Range Core.prototype._toScreen = function (time) { return DateUtil.toScreen(this, time, this.props.center.width); }; /** * Convert a datetime (Date object) into a position on the root * This is used to get the pixel density estimate for the screen, not the center panel * @param {Date} time A date * @return {int} x The position on root in pixels which corresponds * with the given date. * @protected */ // TODO: move this function to Range Core.prototype._toGlobalScreen = function (time) { return DateUtil.toScreen(this, time, this.props.root.width); //var conversion = this.range.conversion(this.props.root.width); //return (time.valueOf() - conversion.offset) * conversion.scale; }; /** * Initialize watching when option autoResize is true * @private */ Core.prototype._initAutoResize = function () { if (this.options.autoResize == true) { this._startAutoResize(); } else { this._stopAutoResize(); } }; /** * Watch for changes in the size of the container. On resize, the Panel will * automatically redraw itself. * @private */ Core.prototype._startAutoResize = function () { var me = this; this._stopAutoResize(); this._onResize = function () { if (me.options.autoResize != true) { // stop watching when the option autoResize is changed to false me._stopAutoResize(); return; } if (me.dom.root) { // check whether the frame is resized // Note: we compare offsetWidth here, not clientWidth. For some reason, // IE does not restore the clientWidth from 0 to the actual width after // changing the timeline's container display style from none to visible if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { me.props.lastWidth = me.dom.root.offsetWidth; me.props.lastHeight = me.dom.root.offsetHeight; me.emit('change'); } } }; // add event listener to window resize util.addEventListener(window, 'resize', this._onResize); this.watchTimer = setInterval(this._onResize, 1000); }; /** * Stop watching for a resize of the frame. * @private */ Core.prototype._stopAutoResize = function () { if (this.watchTimer) { clearInterval(this.watchTimer); this.watchTimer = undefined; } // remove event listener on window.resize util.removeEventListener(window, 'resize', this._onResize); this._onResize = null; }; /** * Start moving the timeline vertically * @param {Event} event * @private */ Core.prototype._onTouch = function (event) { this.touch.allowDragging = true; this.touch.initialScrollTop = this.props.scrollTop; }; /** * Start moving the timeline vertically * @param {Event} event * @private */ Core.prototype._onPinch = function (event) { this.touch.allowDragging = false; }; /** * Move the timeline vertically * @param {Event} event * @private */ Core.prototype._onDrag = function (event) { // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (!this.touch.allowDragging) return; var delta = event.deltaY; var oldScrollTop = this._getScrollTop(); var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); if (newScrollTop != oldScrollTop) { this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already this.emit('verticalDrag'); } }; /** * Apply a scrollTop * @param {Number} scrollTop * @returns {Number} scrollTop Returns the applied scrollTop * @private */ Core.prototype._setScrollTop = function (scrollTop) { this.props.scrollTop = scrollTop; this._updateScrollTop(); return this.props.scrollTop; }; /** * Update the current scrollTop when the height of the containers has been changed * @returns {Number} scrollTop Returns the applied scrollTop * @private */ Core.prototype._updateScrollTop = function () { // recalculate the scrollTopMin var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero if (scrollTopMin != this.props.scrollTopMin) { // in case of bottom orientation, change the scrollTop such that the contents // do not move relative to the time axis at the bottom if (this.options.orientation.item != 'top') { this.props.scrollTop += scrollTopMin - this.props.scrollTopMin; } this.props.scrollTopMin = scrollTopMin; } // limit the scrollTop to the feasible scroll range if (this.props.scrollTop > 0) this.props.scrollTop = 0; if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; return this.props.scrollTop; }; /** * Get the current scrollTop * @returns {number} scrollTop * @private */ Core.prototype._getScrollTop = function () { return this.props.scrollTop; }; /** * Load a configurator * @return {Object} * @private */ Core.prototype._createConfigurator = function () { throw new Error('Cannot invoke abstract method _createConfigurator'); }; module.exports = Core; /***/ }, /* 31 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Hammer = __webpack_require__(23); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var TimeStep = __webpack_require__(36); var Component = __webpack_require__(21); var Group = __webpack_require__(32); var BackgroundGroup = __webpack_require__(37); var BoxItem = __webpack_require__(38); var PointItem = __webpack_require__(39); var RangeItem = __webpack_require__(34); var BackgroundItem = __webpack_require__(40); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items var BACKGROUND = '__background__'; // reserved group id for background items without group /** * An ItemSet holds a set of items and ranges which can be displayed in a * range. The width is determined by the parent of the ItemSet, and the height * is determined by the size of the items. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body * @param {Object} [options] See ItemSet.setOptions for the available options. * @constructor ItemSet * @extends Component */ function ItemSet(body, options) { this.body = body; this.defaultOptions = { type: null, // 'box', 'point', 'range', 'background' orientation: { item: 'bottom' // item orientation: 'top' or 'bottom' }, align: 'auto', // alignment of box items stack: true, groupOrder: null, selectable: true, multiselect: false, editable: { updateTime: false, updateGroup: false, add: false, remove: false }, snap: TimeStep.snap, onAdd: function onAdd(item, callback) { callback(item); }, onUpdate: function onUpdate(item, callback) { callback(item); }, onMove: function onMove(item, callback) { callback(item); }, onRemove: function onRemove(item, callback) { callback(item); }, onMoving: function onMoving(item, callback) { callback(item); }, margin: { item: { horizontal: 10, vertical: 10 }, axis: 20 } }; // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); // options for getting items from the DataSet with the correct type this.itemOptions = { type: { start: 'Date', end: 'Date' } }; this.conversion = { toScreen: body.util.toScreen, toTime: body.util.toTime }; this.dom = {}; this.props = {}; this.hammer = null; var me = this; this.itemsData = null; // DataSet this.groupsData = null; // DataSet // listeners for the DataSet of the items this.itemListeners = { 'add': function add(event, params, senderId) { me._onAdd(params.items); }, 'update': function update(event, params, senderId) { me._onUpdate(params.items); }, 'remove': function remove(event, params, senderId) { me._onRemove(params.items); } }; // listeners for the DataSet of the groups this.groupListeners = { 'add': function add(event, params, senderId) { me._onAddGroups(params.items); }, 'update': function update(event, params, senderId) { me._onUpdateGroups(params.items); }, 'remove': function remove(event, params, senderId) { me._onRemoveGroups(params.items); } }; this.items = {}; // object with an Item for every data item this.groups = {}; // Group object for every group this.groupIds = []; this.selection = []; // list with the ids of all selected nodes this.stackDirty = true; // if true, all items will be restacked on next redraw this.touchParams = {}; // stores properties while dragging // create the HTML DOM this._create(); this.setOptions(options); } ItemSet.prototype = new Component(); // available item types will be registered here ItemSet.types = { background: BackgroundItem, box: BoxItem, range: RangeItem, point: PointItem }; /** * Create the HTML DOM for the ItemSet */ ItemSet.prototype._create = function () { var frame = document.createElement('div'); frame.className = 'vis-itemset'; frame['timeline-itemset'] = this; this.dom.frame = frame; // create background panel var background = document.createElement('div'); background.className = 'vis-background'; frame.appendChild(background); this.dom.background = background; // create foreground panel var foreground = document.createElement('div'); foreground.className = 'vis-foreground'; frame.appendChild(foreground); this.dom.foreground = foreground; // create axis panel var axis = document.createElement('div'); axis.className = 'vis-axis'; this.dom.axis = axis; // create labelset var labelSet = document.createElement('div'); labelSet.className = 'vis-labelset'; this.dom.labelSet = labelSet; // create ungrouped Group this._updateUngrouped(); // create background Group var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); backgroundGroup.show(); this.groups[BACKGROUND] = backgroundGroup; // attach event listeners // Note: we bind to the centerContainer for the case where the height // of the center container is larger than of the ItemSet, so we // can click in the empty area to create a new item or deselect an item. this.hammer = new Hammer(this.body.dom.centerContainer); // drag items when selected this.hammer.on('hammer.input', (function (event) { if (event.isFirst) { this._onTouch(event); } }).bind(this)); this.hammer.on('panstart', this._onDragStart.bind(this)); this.hammer.on('panmove', this._onDrag.bind(this)); this.hammer.on('panend', this._onDragEnd.bind(this)); // single select (or unselect) when tapping an item this.hammer.on('tap', this._onSelectItem.bind(this)); // multi select when holding mouse/touch, or on ctrl+click this.hammer.on('press', this._onMultiSelectItem.bind(this)); // add item on doubletap this.hammer.on('doubletap', this._onAddItem.bind(this)); // attach to the DOM this.show(); }; /** * Set options for the ItemSet. Existing options will be extended/overwritten. * @param {Object} [options] The following options are available: * {String} type * Default type for the items. Choose from 'box' * (default), 'point', 'range', or 'background'. * The default style can be overwritten by * individual items. * {String} align * Alignment for the items, only applicable for * BoxItem. Choose 'center' (default), 'left', or * 'right'. * {String} orientation.item * Orientation of the item set. Choose 'top' or * 'bottom' (default). * {Function} groupOrder * A sorting function for ordering groups * {Boolean} stack * If true (default), items will be stacked on * top of each other. * {Number} margin.axis * Margin between the axis and the items in pixels. * Default is 20. * {Number} margin.item.horizontal * Horizontal margin between items in pixels. * Default is 10. * {Number} margin.item.vertical * Vertical Margin between items in pixels. * Default is 10. * {Number} margin.item * Margin between items in pixels in both horizontal * and vertical direction. Default is 10. * {Number} margin * Set margin for both axis and items in pixels. * {Boolean} selectable * If true (default), items can be selected. * {Boolean} multiselect * If true, multiple items can be selected. * False by default. * {Boolean} editable * Set all editable options to true or false * {Boolean} editable.updateTime * Allow dragging an item to an other moment in time * {Boolean} editable.updateGroup * Allow dragging an item to an other group * {Boolean} editable.add * Allow creating new items on double tap * {Boolean} editable.remove * Allow removing items by clicking the delete button * top right of a selected item. * {Function(item: Item, callback: Function)} onAdd * Callback function triggered when an item is about to be added: * when the user double taps an empty space in the Timeline. * {Function(item: Item, callback: Function)} onUpdate * Callback function fired when an item is about to be updated. * This function typically has to show a dialog where the user * change the item. If not implemented, nothing happens. * {Function(item: Item, callback: Function)} onMove * Fired when an item has been moved. If not implemented, * the move action will be accepted. * {Function(item: Item, callback: Function)} onRemove * Fired when an item is about to be deleted. * If not implemented, the item will be always removed. */ ItemSet.prototype.setOptions = function (options) { if (options) { // copy all options that we know var fields = ['type', 'align', 'order', 'stack', 'selectable', 'multiselect', 'groupOrder', 'dataAttributes', 'template', 'hide', 'snap']; util.selectiveExtend(fields, this.options, options); if ('orientation' in options) { if (typeof options.orientation === 'string') { this.options.orientation.item = options.orientation === 'top' ? 'top' : 'bottom'; } else if (typeof options.orientation === 'object' && 'item' in options.orientation) { this.options.orientation.item = options.orientation.item; } } if ('margin' in options) { if (typeof options.margin === 'number') { this.options.margin.axis = options.margin; this.options.margin.item.horizontal = options.margin; this.options.margin.item.vertical = options.margin; } else if (typeof options.margin === 'object') { util.selectiveExtend(['axis'], this.options.margin, options.margin); if ('item' in options.margin) { if (typeof options.margin.item === 'number') { this.options.margin.item.horizontal = options.margin.item; this.options.margin.item.vertical = options.margin.item; } else if (typeof options.margin.item === 'object') { util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item); } } } } if ('editable' in options) { if (typeof options.editable === 'boolean') { this.options.editable.updateTime = options.editable; this.options.editable.updateGroup = options.editable; this.options.editable.add = options.editable; this.options.editable.remove = options.editable; } else if (typeof options.editable === 'object') { util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); } } // callback functions var addCallback = (function (name) { var fn = options[name]; if (fn) { if (!(fn instanceof Function)) { throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)'); } this.options[name] = fn; } }).bind(this); ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback); // force the itemSet to refresh: options like orientation and margins may be changed this.markDirty(); } }; /** * Mark the ItemSet dirty so it will refresh everything with next redraw. * Optionally, all items can be marked as dirty and be refreshed. * @param {{refreshItems: boolean}} [options] */ ItemSet.prototype.markDirty = function (options) { this.groupIds = []; this.stackDirty = true; if (options && options.refreshItems) { util.forEach(this.items, function (item) { item.dirty = true; if (item.displayed) item.redraw(); }); } }; /** * Destroy the ItemSet */ ItemSet.prototype.destroy = function () { this.hide(); this.setItems(null); this.setGroups(null); this.hammer = null; this.body = null; this.conversion = null; }; /** * Hide the component from the DOM */ ItemSet.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); } // remove the axis with dots if (this.dom.axis.parentNode) { this.dom.axis.parentNode.removeChild(this.dom.axis); } // remove the labelset containing all group labels if (this.dom.labelSet.parentNode) { this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); } }; /** * Show the component in the DOM (when not already visible). * @return {Boolean} changed */ ItemSet.prototype.show = function () { // show frame containing the items if (!this.dom.frame.parentNode) { this.body.dom.center.appendChild(this.dom.frame); } // show axis with dots if (!this.dom.axis.parentNode) { this.body.dom.backgroundVertical.appendChild(this.dom.axis); } // show labelset containing labels if (!this.dom.labelSet.parentNode) { this.body.dom.left.appendChild(this.dom.labelSet); } }; /** * Set selected items by their id. Replaces the current selection * Unknown id's are silently ignored. * @param {string[] | string} [ids] An array with zero or more id's of the items to be * selected, or a single item id. If ids is undefined * or an empty array, all items will be unselected. */ ItemSet.prototype.setSelection = function (ids) { var i, ii, id, item; if (ids == undefined) ids = []; if (!Array.isArray(ids)) ids = [ids]; // unselect currently selected items for (i = 0, ii = this.selection.length; i < ii; i++) { id = this.selection[i]; item = this.items[id]; if (item) item.unselect(); } // select items this.selection = []; for (i = 0, ii = ids.length; i < ii; i++) { id = ids[i]; item = this.items[id]; if (item) { this.selection.push(id); item.select(); } } }; /** * Get the selected items by their id * @return {Array} ids The ids of the selected items */ ItemSet.prototype.getSelection = function () { return this.selection.concat([]); }; /** * Get the id's of the currently visible items. * @returns {Array} The ids of the visible items */ ItemSet.prototype.getVisibleItems = function () { var range = this.body.range.getRange(); var left = this.body.util.toScreen(range.start); var right = this.body.util.toScreen(range.end); var ids = []; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { var group = this.groups[groupId]; var rawVisibleItems = group.visibleItems; // filter the "raw" set with visibleItems into a set which is really // visible by pixels for (var i = 0; i < rawVisibleItems.length; i++) { var item = rawVisibleItems[i]; // TODO: also check whether visible vertically if (item.left < right && item.left + item.width > left) { ids.push(item.id); } } } } return ids; }; /** * Deselect a selected item * @param {String | Number} id * @private */ ItemSet.prototype._deselect = function (id) { var selection = this.selection; for (var i = 0, ii = selection.length; i < ii; i++) { if (selection[i] == id) { // non-strict comparison! selection.splice(i, 1); break; } } }; /** * Repaint the component * @return {boolean} Returns true if the component is resized */ ItemSet.prototype.redraw = function () { var margin = this.options.margin, range = this.body.range, asSize = util.option.asSize, options = this.options, orientation = options.orientation.item, resized = false, frame = this.dom.frame; // recalculate absolute position (before redrawing groups) this.props.top = this.body.domProps.top.height + this.body.domProps.border.top; this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; // update class name frame.className = 'vis-itemset'; // reorder the groups (if needed) resized = this._orderGroups() || resized; // check whether zoomed (in that case we need to re-stack everything) // TODO: would be nicer to get this as a trigger from Range var visibleInterval = range.end - range.start; var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; if (zoomed) this.stackDirty = true; this.lastVisibleInterval = visibleInterval; this.props.lastWidth = this.props.width; var restack = this.stackDirty; var firstGroup = this._firstGroup(); var firstMargin = { item: margin.item, axis: margin.axis }; var nonFirstMargin = { item: margin.item, axis: margin.item.vertical / 2 }; var height = 0; var minHeight = margin.axis + margin.item.vertical; // redraw the background group this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); // redraw all regular groups util.forEach(this.groups, function (group) { var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; var groupResized = group.redraw(range, groupMargin, restack); resized = groupResized || resized; height += group.height; }); height = Math.max(height, minHeight); this.stackDirty = false; // update frame height frame.style.height = asSize(height); // calculate actual size this.props.width = frame.offsetWidth; this.props.height = height; // reposition axis this.dom.axis.style.top = asSize(orientation == 'top' ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); this.dom.axis.style.left = '0'; // check if this component is resized resized = this._isResized() || resized; return resized; }; /** * Get the first group, aligned with the axis * @return {Group | null} firstGroup * @private */ ItemSet.prototype._firstGroup = function () { var firstGroupIndex = this.options.orientation.item == 'top' ? 0 : this.groupIds.length - 1; var firstGroupId = this.groupIds[firstGroupIndex]; var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; return firstGroup || null; }; /** * Create or delete the group holding all ungrouped items. This group is used when * there are no groups specified. * @protected */ ItemSet.prototype._updateUngrouped = function () { var ungrouped = this.groups[UNGROUPED]; var background = this.groups[BACKGROUND]; var item, itemId; if (this.groupsData) { // remove the group holding all ungrouped items if (ungrouped) { ungrouped.hide(); delete this.groups[UNGROUPED]; for (itemId in this.items) { if (this.items.hasOwnProperty(itemId)) { item = this.items[itemId]; item.parent && item.parent.remove(item); var groupId = this._getGroupId(item.data); var group = this.groups[groupId]; group && group.add(item) || item.hide(); } } } } else { // create a group holding all (unfiltered) items if (!ungrouped) { var id = null; var data = null; ungrouped = new Group(id, data, this); this.groups[UNGROUPED] = ungrouped; for (itemId in this.items) { if (this.items.hasOwnProperty(itemId)) { item = this.items[itemId]; ungrouped.add(item); } } ungrouped.show(); } } }; /** * Get the element for the labelset * @return {HTMLElement} labelSet */ ItemSet.prototype.getLabelSet = function () { return this.dom.labelSet; }; /** * Set items * @param {vis.DataSet | null} items */ ItemSet.prototype.setItems = function (items) { var me = this, ids, oldItemsData = this.itemsData; // replace the dataset if (!items) { this.itemsData = null; } else if (items instanceof DataSet || items instanceof DataView) { this.itemsData = items; } else { throw new TypeError('Data must be an instance of DataSet or DataView'); } if (oldItemsData) { // unsubscribe from old dataset util.forEach(this.itemListeners, function (callback, event) { oldItemsData.off(event, callback); }); // remove all drawn items ids = oldItemsData.getIds(); this._onRemove(ids); } if (this.itemsData) { // subscribe to new dataset var id = this.id; util.forEach(this.itemListeners, function (callback, event) { me.itemsData.on(event, callback, id); }); // add all new items ids = this.itemsData.getIds(); this._onAdd(ids); // update the group holding all ungrouped items this._updateUngrouped(); } }; /** * Get the current items * @returns {vis.DataSet | null} */ ItemSet.prototype.getItems = function () { return this.itemsData; }; /** * Set groups * @param {vis.DataSet} groups */ ItemSet.prototype.setGroups = function (groups) { var me = this, ids; // unsubscribe from current dataset if (this.groupsData) { util.forEach(this.groupListeners, function (callback, event) { me.groupsData.off(event, callback); }); // remove all drawn groups ids = this.groupsData.getIds(); this.groupsData = null; this._onRemoveGroups(ids); // note: this will cause a redraw } // replace the dataset if (!groups) { this.groupsData = null; } else if (groups instanceof DataSet || groups instanceof DataView) { this.groupsData = groups; } else { throw new TypeError('Data must be an instance of DataSet or DataView'); } if (this.groupsData) { // subscribe to new dataset var id = this.id; util.forEach(this.groupListeners, function (callback, event) { me.groupsData.on(event, callback, id); }); // draw all ms ids = this.groupsData.getIds(); this._onAddGroups(ids); } // update the group holding all ungrouped items this._updateUngrouped(); // update the order of all items in each group this._order(); this.body.emitter.emit('change', { queue: true }); }; /** * Get the current groups * @returns {vis.DataSet | null} groups */ ItemSet.prototype.getGroups = function () { return this.groupsData; }; /** * Remove an item by its id * @param {String | Number} id */ ItemSet.prototype.removeItem = function (id) { var item = this.itemsData.get(id), dataset = this.itemsData.getDataSet(); if (item) { // confirm deletion this.options.onRemove(item, function (item) { if (item) { // remove by id here, it is possible that an item has no id defined // itself, so better not delete by the item itself dataset.remove(id); } }); } }; /** * Get the time of an item based on it's data and options.type * @param {Object} itemData * @returns {string} Returns the type * @private */ ItemSet.prototype._getType = function (itemData) { return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); }; /** * Get the group id for an item * @param {Object} itemData * @returns {string} Returns the groupId * @private */ ItemSet.prototype._getGroupId = function (itemData) { var type = this._getType(itemData); if (type == 'background' && itemData.group == undefined) { return BACKGROUND; } else { return this.groupsData ? itemData.group : UNGROUPED; } }; /** * Handle updated items * @param {Number[]} ids * @protected */ ItemSet.prototype._onUpdate = function (ids) { var me = this; ids.forEach((function (id) { var itemData = me.itemsData.get(id, me.itemOptions); var item = me.items[id]; var type = me._getType(itemData); var constructor = ItemSet.types[type]; var selected; if (item) { // update item if (!constructor || !(item instanceof constructor)) { // item type has changed, delete the item and recreate it selected = item.selected; // preserve selection of this item me._removeItem(item); item = null; } else { me._updateItem(item, itemData); } } if (!item) { // create item if (constructor) { item = new constructor(itemData, me.conversion, me.options); item.id = id; // TODO: not so nice setting id afterwards me._addItem(item); if (selected) { this.selection.push(id); item.select(); } } else if (type == 'rangeoverflow') { // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' + '.vis-item.vis-range .vis-item-content {overflow: visible;}'); } else { throw new TypeError('Unknown item type "' + type + '"'); } } }).bind(this)); this._order(); this.stackDirty = true; // force re-stacking of all items next redraw this.body.emitter.emit('change', { queue: true }); }; /** * Handle added items * @param {Number[]} ids * @protected */ ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; /** * Handle removed items * @param {Number[]} ids * @protected */ ItemSet.prototype._onRemove = function (ids) { var count = 0; var me = this; ids.forEach(function (id) { var item = me.items[id]; if (item) { count++; me._removeItem(item); } }); if (count) { // update order this._order(); this.stackDirty = true; // force re-stacking of all items next redraw this.body.emitter.emit('change', { queue: true }); } }; /** * Update the order of item in all groups * @private */ ItemSet.prototype._order = function () { // reorder the items in all groups // TODO: optimization: only reorder groups affected by the changed items util.forEach(this.groups, function (group) { group.order(); }); }; /** * Handle updated groups * @param {Number[]} ids * @private */ ItemSet.prototype._onUpdateGroups = function (ids) { this._onAddGroups(ids); }; /** * Handle changed groups (added or updated) * @param {Number[]} ids * @private */ ItemSet.prototype._onAddGroups = function (ids) { var me = this; ids.forEach(function (id) { var groupData = me.groupsData.get(id); var group = me.groups[id]; if (!group) { // check for reserved ids if (id == UNGROUPED || id == BACKGROUND) { throw new Error('Illegal group id. ' + id + ' is a reserved id.'); } var groupOptions = Object.create(me.options); util.extend(groupOptions, { height: null }); group = new Group(id, groupData, me); me.groups[id] = group; // add items with this groupId to the new group for (var itemId in me.items) { if (me.items.hasOwnProperty(itemId)) { var item = me.items[itemId]; if (item.data.group == id) { group.add(item); } } } group.order(); group.show(); } else { // update group group.setData(groupData); } }); this.body.emitter.emit('change', { queue: true }); }; /** * Handle removed groups * @param {Number[]} ids * @private */ ItemSet.prototype._onRemoveGroups = function (ids) { var groups = this.groups; ids.forEach(function (id) { var group = groups[id]; if (group) { group.hide(); delete groups[id]; } }); this.markDirty(); this.body.emitter.emit('change', { queue: true }); }; /** * Reorder the groups if needed * @return {boolean} changed * @private */ ItemSet.prototype._orderGroups = function () { if (this.groupsData) { // reorder the groups var groupIds = this.groupsData.getIds({ order: this.options.groupOrder }); var changed = !util.equalArray(groupIds, this.groupIds); if (changed) { // hide all groups, removes them from the DOM var groups = this.groups; groupIds.forEach(function (groupId) { groups[groupId].hide(); }); // show the groups again, attach them to the DOM in correct order groupIds.forEach(function (groupId) { groups[groupId].show(); }); this.groupIds = groupIds; } return changed; } else { return false; } }; /** * Add a new item * @param {Item} item * @private */ ItemSet.prototype._addItem = function (item) { this.items[item.id] = item; // add to group var groupId = this._getGroupId(item.data); var group = this.groups[groupId]; if (group) group.add(item); }; /** * Update an existing item * @param {Item} item * @param {Object} itemData * @private */ ItemSet.prototype._updateItem = function (item, itemData) { var oldGroupId = item.data.group; var oldSubGroupId = item.data.subgroup; // update the items data (will redraw the item when displayed) item.setData(itemData); // update group if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { var oldGroup = this.groups[oldGroupId]; if (oldGroup) oldGroup.remove(item); var groupId = this._getGroupId(item.data); var group = this.groups[groupId]; if (group) group.add(item); } }; /** * Delete an item from the ItemSet: remove it from the DOM, from the map * with items, and from the map with visible items, and from the selection * @param {Item} item * @private */ ItemSet.prototype._removeItem = function (item) { // remove from DOM item.hide(); // remove from items delete this.items[item.id]; // remove from selection var index = this.selection.indexOf(item.id); if (index != -1) this.selection.splice(index, 1); // remove from group item.parent && item.parent.remove(item); }; /** * Create an array containing all items being a range (having an end date) * @param array * @returns {Array} * @private */ ItemSet.prototype._constructByEndArray = function (array) { var endArray = []; for (var i = 0; i < array.length; i++) { if (array[i] instanceof RangeItem) { endArray.push(array[i]); } } return endArray; }; /** * Register the clicked item on touch, before dragStart is initiated. * * dragStart is initiated from a mousemove event, AFTER the mouse/touch is * already moving. Therefore, the mouse/touch can sometimes be above an other * DOM element than the item itself. * * @param {Event} event * @private */ ItemSet.prototype._onTouch = function (event) { // store the touched item, used in _onDragStart this.touchParams.item = this.itemFromTarget(event); this.touchParams.dragLeftItem = event.target.dragLeftItem || false; this.touchParams.dragRightItem = event.target.dragRightItem || false; this.touchParams.itemProps = null; }; /** * Start dragging the selected events * @param {Event} event * @private */ ItemSet.prototype._onDragStart = function (event) { var item = this.touchParams.item || null; var me = this; var props; if (item && item.selected) { if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { return; } // override options.editable if (item.editable === false) { return; } var dragLeftItem = this.touchParams.dragLeftItem; var dragRightItem = this.touchParams.dragRightItem; if (dragLeftItem) { props = { item: dragLeftItem, initialX: event.center.x, dragLeft: true, data: util.extend({}, item.data) // clone the items data }; this.touchParams.itemProps = [props]; } else if (dragRightItem) { props = { item: dragRightItem, initialX: event.center.x, dragRight: true, data: util.extend({}, item.data) // clone the items data }; this.touchParams.itemProps = [props]; } else { this.touchParams.itemProps = this.getSelection().map(function (id) { var item = me.items[id]; var props = { item: item, initialX: event.center.x, data: util.extend({}, item.data) // clone the items data }; return props; }); } event.stopPropagation(); } else if (this.options.editable.add && (event.srcEvent.ctrlKey || event.srcEvent.metaKey)) { // create a new range item when dragging with ctrl key down this._onDragStartAddItem(event); } }; /** * Start creating a new range item by dragging. * @param {Event} event * @private */ ItemSet.prototype._onDragStartAddItem = function (event) { var snap = this.options.snap || null; var xAbs = util.getAbsoluteLeft(this.dom.frame); var x = event.center.x - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px var time = this.body.util.toTime(x); var scale = this.body.util.getScale(); var step = this.body.util.getStep(); var start = snap ? snap(time, scale, step) : start; var end = start; var itemData = { type: 'range', start: start, end: end, content: 'new item' }; var id = util.randomUUID(); itemData[this.itemsData._fieldId] = id; var group = this.groupFromTarget(event); if (group) { itemData.group = group.groupId; } var newItem = new RangeItem(itemData, this.conversion, this.options); newItem.id = id; // TODO: not so nice setting id afterwards newItem.data = itemData; this._addItem(newItem); var props = { item: newItem, dragRight: true, initialX: event.center.x, data: util.extend({}, itemData) }; this.touchParams.itemProps = [props]; event.stopPropagation(); }; /** * Drag selected items * @param {Event} event * @private */ ItemSet.prototype._onDrag = function (event) { if (this.touchParams.itemProps) { event.stopPropagation(); var me = this; var snap = this.options.snap || null; var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; var scale = this.body.util.getScale(); var step = this.body.util.getStep(); // move this.touchParams.itemProps.forEach(function (props) { var newProps = {}; var current = me.body.util.toTime(event.center.x - xOffset); var initial = me.body.util.toTime(props.initialX - xOffset); var offset = current - initial; var itemData = util.extend({}, props.item.data); // clone the data if (props.item.editable === false) { return; } var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; if (updateTimeAllowed) { if (props.dragLeft) { // drag left side of a range item if (itemData.start != undefined) { var initialStart = util.convert(props.data.start, 'Date'); var start = new Date(initialStart.valueOf() + offset); itemData.start = snap ? snap(start, scale, step) : start; } } else if (props.dragRight) { // drag right side of a range item if (itemData.end != undefined) { var initialEnd = util.convert(props.data.end, 'Date'); var end = new Date(initialEnd.valueOf() + offset); itemData.end = snap ? snap(end, scale, step) : end; } } else { // drag both start and end if (itemData.start != undefined) { var initialStart = util.convert(props.data.start, 'Date').valueOf(); var start = new Date(initialStart + offset); if (itemData.end != undefined) { var initialEnd = util.convert(props.data.end, 'Date'); var duration = initialEnd.valueOf() - initialStart.valueOf(); itemData.start = snap ? snap(start, scale, step) : start; itemData.end = new Date(itemData.start.valueOf() + duration); } else { itemData.start = snap ? snap(start, scale, step) : start; } } } } var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; if (updateGroupAllowed && (!props.dragLeft && !props.dragRight)) { if (itemData.group != undefined) { // drag from one group to another var group = me.groupFromTarget(event); if (group) { itemData.group = group.groupId; } } } // confirm moving the item me.options.onMoving(itemData, function (itemData) { if (itemData) { props.item.setData(itemData); } }); }); this.stackDirty = true; // force re-stacking of all items next redraw this.body.emitter.emit('change'); } }; /** * Move an item to another group * @param {Item} item * @param {String | Number} groupId * @private */ ItemSet.prototype._moveToGroup = function (item, groupId) { var group = this.groups[groupId]; if (group && group.groupId != item.data.group) { var oldGroup = item.parent; oldGroup.remove(item); oldGroup.order(); group.add(item); group.order(); item.data.group = group.groupId; } }; /** * End of dragging selected items * @param {Event} event * @private */ ItemSet.prototype._onDragEnd = function (event) { if (this.touchParams.itemProps) { event.stopPropagation(); // prepare a change set for the changed items var changes = []; var me = this; var dataset = this.itemsData.getDataSet(); var itemProps = this.touchParams.itemProps; this.touchParams.itemProps = null; itemProps.forEach(function (props) { var id = props.item.id; var exists = me.itemsData.get(id, me.itemOptions) != null; if (!exists) { // add a new item me.options.onAdd(props.item.data, function (itemData) { me._removeItem(props.item); // remove temporary item if (itemData) { me.itemsData.getDataSet().add(itemData); } // force re-stacking of all items next redraw me.stackDirty = true; me.body.emitter.emit('change'); }); } else { // update existing item var itemData = util.extend({}, props.item.data); // clone the data me.options.onMove(itemData, function (itemData) { if (itemData) { // apply changes itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) changes.push(itemData); } else { // restore original values props.item.setData(props.data); me.stackDirty = true; // force re-stacking of all items next redraw me.body.emitter.emit('change'); } }); } }); // apply the changes to the data (if there are changes) if (changes.length) { dataset.update(changes); } } }; /** * Handle selecting/deselecting an item when tapping it * @param {Event} event * @private */ ItemSet.prototype._onSelectItem = function (event) { if (!this.options.selectable) return; var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); var shiftKey = event.srcEvent && event.srcEvent.shiftKey; if (ctrlKey || shiftKey) { this._onMultiSelectItem(event); return; } var oldSelection = this.getSelection(); var item = this.itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); var newSelection = this.getSelection(); // emit a select event, // except when old selection is empty and new selection is still empty if (newSelection.length > 0 || oldSelection.length > 0) { this.body.emitter.emit('select', { items: newSelection, event: event }); } }; /** * Handle creation and updates of an item on double tap * @param event * @private */ ItemSet.prototype._onAddItem = function (event) { if (!this.options.selectable) return; if (!this.options.editable.add) return; var me = this; var snap = this.options.snap || null; var item = this.itemFromTarget(event); event.stopPropagation(); if (item) { // update item // execute async handler to update the item (or cancel it) var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset this.options.onUpdate(itemData, function (itemData) { if (itemData) { me.itemsData.getDataSet().update(itemData); } }); } else { // add item var xAbs = util.getAbsoluteLeft(this.dom.frame); var x = event.center.x - xAbs; var start = this.body.util.toTime(x); var scale = this.body.util.getScale(); var step = this.body.util.getStep(); var newItem = { start: snap ? snap(start, scale, step) : start, content: 'new item' }; // when default type is a range, add a default end date to the new item if (this.options.type === 'range') { var end = this.body.util.toTime(x + this.props.width / 5); newItem.end = snap ? snap(end, scale, step) : end; } newItem[this.itemsData._fieldId] = util.randomUUID(); var group = this.groupFromTarget(event); if (group) { newItem.group = group.groupId; } // execute async handler to customize (or cancel) adding an item this.options.onAdd(newItem, function (item) { if (item) { me.itemsData.getDataSet().add(item); // TODO: need to trigger a redraw? } }); } }; /** * Handle selecting/deselecting multiple items when holding an item * @param {Event} event * @private */ ItemSet.prototype._onMultiSelectItem = function (event) { if (!this.options.selectable) return; var item = this.itemFromTarget(event); if (item) { // multi select items (if allowed) var selection = this.options.multiselect ? this.getSelection() // take current selection : []; // deselect current selection var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; if (shiftKey && this.options.multiselect) { // select all items between the old selection and the tapped item // determine the selection range selection.push(item.id); var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); // select all items within the selection range selection = []; for (var id in this.items) { if (this.items.hasOwnProperty(id)) { var _item = this.items[id]; var start = _item.data.start; var end = _item.data.end !== undefined ? _item.data.end : start; if (start >= range.min && end <= range.max && !(_item instanceof BackgroundItem)) { selection.push(_item.id); // do not use id but item.id, id itself is stringified } } } } else { // add/remove this item from the current selection var index = selection.indexOf(item.id); if (index == -1) { // item is not yet selected -> select it selection.push(item.id); } else { // item is already selected -> deselect it selection.splice(index, 1); } } this.setSelection(selection); this.body.emitter.emit('select', { items: this.getSelection(), event: event }); } }; /** * Calculate the time range of a list of items * @param {Array.} itemsData * @return {{min: Date, max: Date}} Returns the range of the provided items * @private */ ItemSet._getItemRange = function (itemsData) { var max = null; var min = null; itemsData.forEach(function (data) { if (min == null || data.start < min) { min = data.start; } if (data.end != undefined) { if (max == null || data.end > max) { max = data.end; } } else { if (max == null || data.start > max) { max = data.start; } } }); return { min: min, max: max }; }; /** * Find an item from an event target: * searches for the attribute 'timeline-item' in the event target's element tree * @param {Event} event * @return {Item | null} item */ ItemSet.prototype.itemFromTarget = function (event) { var target = event.target; while (target) { if (target.hasOwnProperty('timeline-item')) { return target['timeline-item']; } target = target.parentNode; } return null; }; /** * Find the Group from an event target: * searches for the attribute 'timeline-group' in the event target's element tree * @param {Event} event * @return {Group | null} group */ ItemSet.prototype.groupFromTarget = function (event) { var clientY = event.center ? event.center.y : event.clientY; for (var i = 0; i < this.groupIds.length; i++) { var groupId = this.groupIds[i]; var group = this.groups[groupId]; var foreground = group.dom.foreground; var top = util.getAbsoluteTop(foreground); if (clientY > top && clientY < top + foreground.offsetHeight) { return group; } if (this.options.orientation.item === 'top') { if (i === this.groupIds.length - 1 && clientY > top) { return group; } } else { if (i === 0 && clientY < top + foreground.offset) { return group; } } } return null; }; /** * Find the ItemSet from an event target: * searches for the attribute 'timeline-itemset' in the event target's element tree * @param {Event} event * @return {ItemSet | null} item */ ItemSet.itemSetFromTarget = function (event) { var target = event.target; while (target) { if (target.hasOwnProperty('timeline-itemset')) { return target['timeline-itemset']; } target = target.parentNode; } return null; }; module.exports = ItemSet; /***/ }, /* 32 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var stack = __webpack_require__(33); var RangeItem = __webpack_require__(34); /** * @constructor Group * @param {Number | String} groupId * @param {Object} data * @param {ItemSet} itemSet */ function Group(groupId, data, itemSet) { this.groupId = groupId; this.subgroups = {}; this.subgroupIndex = 0; this.subgroupOrderer = data && data.subgroupOrder; this.itemSet = itemSet; this.dom = {}; this.props = { label: { width: 0, height: 0 } }; this.className = null; this.items = {}; // items filtered by groupId of this group this.visibleItems = []; // items currently visible in window this.orderedItems = { byStart: [], byEnd: [] }; this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap. var me = this; this.itemSet.body.emitter.on('checkRangedItems', function () { me.checkRangedItems = true; }); this._create(); this.setData(data); } /** * Create DOM elements for the group * @private */ Group.prototype._create = function () { var label = document.createElement('div'); label.className = 'vis-label'; this.dom.label = label; var inner = document.createElement('div'); inner.className = 'vis-inner'; label.appendChild(inner); this.dom.inner = inner; var foreground = document.createElement('div'); foreground.className = 'vis-group'; foreground['timeline-group'] = this; this.dom.foreground = foreground; this.dom.background = document.createElement('div'); this.dom.background.className = 'vis-group'; this.dom.axis = document.createElement('div'); this.dom.axis.className = 'vis-group'; // create a hidden marker to detect when the Timelines container is attached // to the DOM, or the style of a parent of the Timeline is changed from // display:none is changed to visible. this.dom.marker = document.createElement('div'); this.dom.marker.style.visibility = 'hidden'; this.dom.marker.innerHTML = '?'; this.dom.background.appendChild(this.dom.marker); }; /** * Set the group data for this group * @param {Object} data Group data, can contain properties content and className */ Group.prototype.setData = function (data) { // update contents var content = data && data.content; if (content instanceof Element) { this.dom.inner.appendChild(content); } else if (content !== undefined && content !== null) { this.dom.inner.innerHTML = content; } else { this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null } // update title this.dom.label.title = data && data.title || ''; if (!this.dom.inner.firstChild) { util.addClassName(this.dom.inner, 'vis-hidden'); } else { util.removeClassName(this.dom.inner, 'vis-hidden'); } // update className var className = data && data.className || null; if (className != this.className) { if (this.className) { util.removeClassName(this.dom.label, this.className); util.removeClassName(this.dom.foreground, this.className); util.removeClassName(this.dom.background, this.className); util.removeClassName(this.dom.axis, this.className); } util.addClassName(this.dom.label, className); util.addClassName(this.dom.foreground, className); util.addClassName(this.dom.background, className); util.addClassName(this.dom.axis, className); this.className = className; } // update style if (this.style) { util.removeCssText(this.dom.label, 'vis-' + this.style); this.style = null; } if (data && data.style) { util.addCssText(this.dom.label, 'vis-' + data.style); this.style = data.style; } }; /** * Get the width of the group label * @return {number} width */ Group.prototype.getLabelWidth = function () { return this.props.label.width; }; /** * Repaint this group * @param {{start: number, end: number}} range * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * @param {boolean} [restack=false] Force restacking of all items * @return {boolean} Returns true if the group is resized */ Group.prototype.redraw = function (range, margin, restack) { var resized = false; // force recalculation of the height of the items when the marker height changed // (due to the Timeline being attached to the DOM or changed from display:none to visible) var markerHeight = this.dom.marker.clientHeight; if (markerHeight != this.lastMarkerHeight) { this.lastMarkerHeight = markerHeight; util.forEach(this.items, function (item) { item.dirty = true; if (item.displayed) item.redraw(); }); restack = true; } // reposition visible items vertically if (typeof this.itemSet.options.order === 'function') { // a custom order function if (restack) { // brute force restack of all items // show all items var me = this; var limitSize = false; util.forEach(this.items, function (item) { if (!item.displayed) { item.redraw(); me.visibleItems.push(item); } item.repositionX(limitSize); }); // order all items and force a restacking var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) { return me.itemSet.options.order(a.data, b.data); }); stack.stack(customOrderedItems, margin, true /* restack=true */); } this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); } else { // no custom order function, lazy stacking this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); if (this.itemSet.options.stack) { // TODO: ugly way to access options... stack.stack(this.visibleItems, margin, restack); } else { // no stacking stack.nostack(this.visibleItems, margin, this.subgroups); } } // recalculate the height of the group var height = this._calculateHeight(margin); // calculate actual size and position var foreground = this.dom.foreground; this.top = foreground.offsetTop; this.left = foreground.offsetLeft; this.width = foreground.offsetWidth; resized = util.updateProperty(this, 'height', height) || resized; // recalculate size of label resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized; resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized; // apply new height this.dom.background.style.height = height + 'px'; this.dom.foreground.style.height = height + 'px'; this.dom.label.style.height = height + 'px'; // update vertical position of items after they are re-stacked and the height of the group is calculated for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { var item = this.visibleItems[i]; item.repositionY(margin); } return resized; }; /** * recalculate the height of the group * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * @returns {number} Returns the height * @private */ Group.prototype._calculateHeight = function (margin) { // recalculate the height of the group var height; var visibleItems = this.visibleItems; //var visibleSubgroups = []; //this.visibleSubgroups = 0; this.resetSubgroups(); var me = this; if (visibleItems.length > 0) { var min = visibleItems[0].top; var max = visibleItems[0].top + visibleItems[0].height; util.forEach(visibleItems, function (item) { min = Math.min(min, item.top); max = Math.max(max, item.top + item.height); if (item.data.subgroup !== undefined) { me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); me.subgroups[item.data.subgroup].visible = true; } }); if (min > margin.axis) { // there is an empty gap between the lowest item and the axis var offset = min - margin.axis; max -= offset; util.forEach(visibleItems, function (item) { item.top -= offset; }); } height = max + margin.item.vertical / 2; } else { height = 0; } height = Math.max(height, this.props.label.height); return height; }; /** * Show this group: attach to the DOM */ Group.prototype.show = function () { if (!this.dom.label.parentNode) { this.itemSet.dom.labelSet.appendChild(this.dom.label); } if (!this.dom.foreground.parentNode) { this.itemSet.dom.foreground.appendChild(this.dom.foreground); } if (!this.dom.background.parentNode) { this.itemSet.dom.background.appendChild(this.dom.background); } if (!this.dom.axis.parentNode) { this.itemSet.dom.axis.appendChild(this.dom.axis); } }; /** * Hide this group: remove from the DOM */ Group.prototype.hide = function () { var label = this.dom.label; if (label.parentNode) { label.parentNode.removeChild(label); } var foreground = this.dom.foreground; if (foreground.parentNode) { foreground.parentNode.removeChild(foreground); } var background = this.dom.background; if (background.parentNode) { background.parentNode.removeChild(background); } var axis = this.dom.axis; if (axis.parentNode) { axis.parentNode.removeChild(axis); } }; /** * Add an item to the group * @param {Item} item */ Group.prototype.add = function (item) { this.items[item.id] = item; item.setParent(this); // add to if (item.data.subgroup !== undefined) { if (this.subgroups[item.data.subgroup] === undefined) { this.subgroups[item.data.subgroup] = { height: 0, visible: false, index: this.subgroupIndex, items: [] }; this.subgroupIndex++; } this.subgroups[item.data.subgroup].items.push(item); } this.orderSubgroups(); if (this.visibleItems.indexOf(item) == -1) { var range = this.itemSet.body.range; // TODO: not nice accessing the range like this this._checkIfVisible(item, this.visibleItems, range); } }; Group.prototype.orderSubgroups = function () { if (this.subgroupOrderer !== undefined) { var sortArray = []; if (typeof this.subgroupOrderer == 'string') { for (var subgroup in this.subgroups) { sortArray.push({ subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer] }); } sortArray.sort(function (a, b) { return a.sortField - b.sortField; }); } else if (typeof this.subgroupOrderer == 'function') { for (var subgroup in this.subgroups) { sortArray.push(this.subgroups[subgroup].items[0].data); } sortArray.sort(this.subgroupOrderer); } if (sortArray.length > 0) { for (var i = 0; i < sortArray.length; i++) { this.subgroups[sortArray[i].subgroup].index = i; } } } }; Group.prototype.resetSubgroups = function () { for (var subgroup in this.subgroups) { if (this.subgroups.hasOwnProperty(subgroup)) { this.subgroups[subgroup].visible = false; } } }; /** * Remove an item from the group * @param {Item} item */ Group.prototype.remove = function (item) { delete this.items[item.id]; item.setParent(null); // remove from visible items var index = this.visibleItems.indexOf(item); if (index != -1) this.visibleItems.splice(index, 1); // TODO: also remove from ordered items? }; /** * Remove an item from the corresponding DataSet * @param {Item} item */ Group.prototype.removeFromDataSet = function (item) { this.itemSet.removeItem(item.id); }; /** * Reorder the items */ Group.prototype.order = function () { var array = util.toArray(this.items); var startArray = []; var endArray = []; for (var i = 0; i < array.length; i++) { if (array[i].data.end !== undefined) { endArray.push(array[i]); } startArray.push(array[i]); } this.orderedItems = { byStart: startArray, byEnd: endArray }; stack.orderByStart(this.orderedItems.byStart); stack.orderByEnd(this.orderedItems.byEnd); }; /** * Update the visible items * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date * @param {Item[]} visibleItems The previously visible items. * @param {{start: number, end: number}} range Visible range * @return {Item[]} visibleItems The new visible items. * @private */ Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { var visibleItems = []; var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems var interval = (range.end - range.start) / 4; var lowerBound = range.start - interval; var upperBound = range.end + interval; var item, i; // this function is used to do the binary search. var searchFunction = function searchFunction(value) { if (value < lowerBound) { return -1; } else if (value <= upperBound) { return 0; } else { return 1; } }; // first check if the items that were in view previously are still in view. // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! // also cleans up invisible items. if (oldVisibleItems.length > 0) { for (i = 0; i < oldVisibleItems.length; i++) { this._checkIfVisibleWithReference(oldVisibleItems[i], visibleItems, visibleItemsLookup, range); } } // we do a binary search for the items that have only start values. var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data', 'start'); // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { return item.data.start < lowerBound || item.data.start > upperBound; }); // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. // We therefore have to brute force check all items in the byEnd list if (this.checkRangedItems == true) { this.checkRangedItems = false; for (i = 0; i < orderedItems.byEnd.length; i++) { this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); } } else { // we do a binary search for the items that have defined end times. var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data', 'end'); // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { return item.data.end < lowerBound || item.data.end > upperBound; }); } // finally, we reposition all the visible items. for (i = 0; i < visibleItems.length; i++) { item = visibleItems[i]; if (!item.displayed) item.show(); // reposition item horizontally item.repositionX(); } // debug //console.log("new line") //if (this.groupId == null) { // for (i = 0; i < orderedItems.byStart.length; i++) { // item = orderedItems.byStart[i].data; // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") // } // for (i = 0; i < orderedItems.byEnd.length; i++) { // item = orderedItems.byEnd[i].data; // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") // } //} return visibleItems; }; Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { var item; var i; if (initialPos != -1) { for (i = initialPos; i >= 0; i--) { item = items[i]; if (breakCondition(item)) { break; } else { if (visibleItemsLookup[item.id] === undefined) { visibleItemsLookup[item.id] = true; visibleItems.push(item); } } } for (i = initialPos + 1; i < items.length; i++) { item = items[i]; if (breakCondition(item)) { break; } else { if (visibleItemsLookup[item.id] === undefined) { visibleItemsLookup[item.id] = true; visibleItems.push(item); } } } } }; /** * this function is very similar to the _checkIfInvisible() but it does not * return booleans, hides the item if it should not be seen and always adds to * the visibleItems. * this one is for brute forcing and hiding. * * @param {Item} item * @param {Array} visibleItems * @param {{start:number, end:number}} range * @private */ Group.prototype._checkIfVisible = function (item, visibleItems, range) { if (item.isVisible(range)) { if (!item.displayed) item.show(); // reposition item horizontally item.repositionX(); visibleItems.push(item); } else { if (item.displayed) item.hide(); } }; /** * this function is very similar to the _checkIfInvisible() but it does not * return booleans, hides the item if it should not be seen and always adds to * the visibleItems. * this one is for brute forcing and hiding. * * @param {Item} item * @param {Array} visibleItems * @param {{start:number, end:number}} range * @private */ Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { if (item.isVisible(range)) { if (visibleItemsLookup[item.id] === undefined) { visibleItemsLookup[item.id] = true; visibleItems.push(item); } } else { if (item.displayed) item.hide(); } }; module.exports = Group; /***/ }, /* 33 */ /***/ function(module, exports) { // Utility functions for ordering and stacking of items 'use strict'; var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** * Order items by their start data * @param {Item[]} items */ exports.orderByStart = function (items) { items.sort(function (a, b) { return a.data.start - b.data.start; }); }; /** * Order items by their end date. If they have no end date, their start date * is used. * @param {Item[]} items */ exports.orderByEnd = function (items) { items.sort(function (a, b) { var aTime = 'end' in a.data ? a.data.end : a.data.start, bTime = 'end' in b.data ? b.data.end : b.data.start; return aTime - bTime; }); }; /** * Adjust vertical positions of the items such that they don't overlap each * other. * @param {Item[]} items * All visible items * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. * @param {boolean} [force=false] * If true, all items will be repositioned. If false (default), only * items having a top===null will be re-stacked */ exports.stack = function (items, margin, force) { var i, iMax; if (force) { // reset top position of all items for (i = 0, iMax = items.length; i < iMax; i++) { items[i].top = null; } } // calculate new, non-overlapping positions for (i = 0, iMax = items.length; i < iMax; i++) { var item = items[i]; if (item.stack && item.top === null) { // initialize top position item.top = margin.axis; do { // TODO: optimize checking for overlap. when there is a gap without items, // you only need to check for items from the next item on, not from zero var collidingItem = null; for (var j = 0, jj = items.length; j < jj; j++) { var other = items[j]; if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item)) { collidingItem = other; break; } } if (collidingItem != null) { // There is a collision. Reposition the items above the colliding element item.top = collidingItem.top + collidingItem.height + margin.item.vertical; } } while (collidingItem); } } }; /** * Adjust vertical positions of the items without stacking them * @param {Item[]} items * All visible items * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. */ exports.nostack = function (items, margin, subgroups) { var i, iMax, newTop; // reset top position of all items for (i = 0, iMax = items.length; i < iMax; i++) { if (items[i].data.subgroup !== undefined) { newTop = margin.axis; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { newTop += subgroups[subgroup].height + margin.item.vertical; } } } items[i].top = newTop; } else { items[i].top = margin.axis; } } }; /** * Test if the two provided items collide * The items must have parameters left, width, top, and height. * @param {Item} a The first item * @param {Item} b The second item * @param {{horizontal: number, vertical: number}} margin * An object containing a horizontal and vertical * minimum required margin. * @return {boolean} true if a and b collide, else false */ exports.collision = function (a, b, margin) { return a.left - margin.horizontal + EPSILON < b.left + b.width && a.left + a.width + margin.horizontal - EPSILON > b.left && a.top - margin.vertical + EPSILON < b.top + b.height && a.top + a.height + margin.vertical - EPSILON > b.top; }; /***/ }, /* 34 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Hammer = __webpack_require__(23); var Item = __webpack_require__(35); /** * @constructor RangeItem * @extends Item * @param {Object} data Object containing parameters start, end * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options * // TODO: describe options */ function RangeItem(data, conversion, options) { this.props = { content: { width: 0 } }; this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { if (data.start == undefined) { throw new Error('Property "start" missing in item ' + data.id); } if (data.end == undefined) { throw new Error('Property "end" missing in item ' + data.id); } } Item.call(this, data, conversion, options); } RangeItem.prototype = new Item(null, null, null); RangeItem.prototype.baseClassName = 'vis-item vis-range'; /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ RangeItem.prototype.isVisible = function (range) { // determine visibility return this.data.start < range.end && this.data.end > range.start; }; /** * Repaint the item */ RangeItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; // background box dom.box = document.createElement('div'); // className is updated in redraw() // frame box (to prevent the item contents from overflowing dom.frame = document.createElement('div'); dom.frame.className = 'vis-item-overflow'; dom.box.appendChild(dom.frame); // contents box dom.content = document.createElement('div'); dom.content.className = 'vis-item-content'; dom.frame.appendChild(dom.content); // attach this item as attribute dom.box['timeline-item'] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { throw new Error('Cannot redraw item: parent has no foreground container element'); } foreground.appendChild(dom.box); } this.displayed = true; // Update DOM when item is marked dirty. An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); this._updateTitle(this.dom.box); this._updateDataAttributes(this.dom.box); this._updateStyle(this.dom.box); var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; // update class var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); dom.box.className = this.baseClassName + className; // determine from css whether this box has overflow this.overflow = window.getComputedStyle(dom.frame).overflow !== 'hidden'; // recalculate size // turn off max-width to be able to calculate the real width // this causes an extra browser repaint/reflow, but so be it this.dom.content.style.maxWidth = 'none'; this.props.content.width = this.dom.content.offsetWidth; this.height = this.dom.box.offsetHeight; this.dom.content.style.maxWidth = ''; this.dirty = false; } this._repaintDeleteButton(dom.box); this._repaintDragLeft(); this._repaintDragRight(); }; /** * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ RangeItem.prototype.show = function () { if (!this.displayed) { this.redraw(); } }; /** * Hide the item from the DOM (when visible) * @return {Boolean} changed */ RangeItem.prototype.hide = function () { if (this.displayed) { var box = this.dom.box; if (box.parentNode) { box.parentNode.removeChild(box); } this.displayed = false; } }; /** * Reposition the item horizontally * @param {boolean} [limitSize=true] If true (default), the width of the range * item will be limited, as the browser cannot * display very wide divs. This means though * that the applied left and width may * not correspond to the ranges start and end * @Override */ RangeItem.prototype.repositionX = function (limitSize) { var parentWidth = this.parent.width; var start = this.conversion.toScreen(this.data.start); var end = this.conversion.toScreen(this.data.end); var contentLeft; var contentWidth; // limit the width of the range, as browsers cannot draw very wide divs if (limitSize === undefined || limitSize === true) { if (start < -parentWidth) { start = -parentWidth; } if (end > 2 * parentWidth) { end = 2 * parentWidth; } } var boxWidth = Math.max(end - start, 1); if (this.overflow) { this.left = start; this.width = boxWidth + this.props.content.width; contentWidth = this.props.content.width; // Note: The calculation of width is an optimistic calculation, giving // a width which will not change when moving the Timeline // So no re-stacking needed, which is nicer for the eye; } else { this.left = start; this.width = boxWidth; contentWidth = Math.min(end - start, this.props.content.width); } this.dom.box.style.left = this.left + 'px'; this.dom.box.style.width = boxWidth + 'px'; switch (this.options.align) { case 'left': this.dom.content.style.left = '0'; break; case 'right': this.dom.content.style.left = Math.max(boxWidth - contentWidth, 0) + 'px'; break; case 'center': this.dom.content.style.left = Math.max((boxWidth - contentWidth) / 2, 0) + 'px'; break; default: // 'auto' // when range exceeds left of the window, position the contents at the left of the visible area if (this.overflow) { if (end > 0) { contentLeft = Math.max(-start, 0); } else { contentLeft = -contentWidth; // ensure it's not visible anymore } } else { if (start < 0) { contentLeft = -start; } else { contentLeft = 0; } } this.dom.content.style.left = contentLeft + 'px'; } }; /** * Reposition the item vertically * @Override */ RangeItem.prototype.repositionY = function () { var orientation = this.options.orientation.item; var box = this.dom.box; if (orientation == 'top') { box.style.top = this.top + 'px'; } else { box.style.top = this.parent.height - this.top - this.height + 'px'; } }; /** * Repaint a drag area on the left side of the range when the range is selected * @protected */ RangeItem.prototype._repaintDragLeft = function () { if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { // create and show drag area var dragLeft = document.createElement('div'); dragLeft.className = 'vis-drag-left'; dragLeft.dragLeftItem = this; this.dom.box.appendChild(dragLeft); this.dom.dragLeft = dragLeft; } else if (!this.selected && this.dom.dragLeft) { // delete drag area if (this.dom.dragLeft.parentNode) { this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); } this.dom.dragLeft = null; } }; /** * Repaint a drag area on the right side of the range when the range is selected * @protected */ RangeItem.prototype._repaintDragRight = function () { if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { // create and show drag area var dragRight = document.createElement('div'); dragRight.className = 'vis-drag-right'; dragRight.dragRightItem = this; this.dom.box.appendChild(dragRight); this.dom.dragRight = dragRight; } else if (!this.selected && this.dom.dragRight) { // delete drag area if (this.dom.dragRight.parentNode) { this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); } this.dom.dragRight = null; } }; module.exports = RangeItem; /***/ }, /* 35 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Hammer = __webpack_require__(23); var util = __webpack_require__(1); /** * @constructor Item * @param {Object} data Object containing (optional) parameters type, * start, end, content, group, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} options Configuration options * // TODO: describe available options */ function Item(data, conversion, options) { this.id = null; this.parent = null; this.data = data; this.dom = null; this.conversion = conversion || {}; this.options = options || {}; this.selected = false; this.displayed = false; this.dirty = true; this.top = null; this.left = null; this.width = null; this.height = null; this.editable = null; if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { this.editable = data.editable; } } Item.prototype.stack = true; /** * Select current item */ Item.prototype.select = function () { this.selected = true; this.dirty = true; if (this.displayed) this.redraw(); }; /** * Unselect current item */ Item.prototype.unselect = function () { this.selected = false; this.dirty = true; if (this.displayed) this.redraw(); }; /** * Set data for the item. Existing data will be updated. The id should not * be changed. When the item is displayed, it will be redrawn immediately. * @param {Object} data */ Item.prototype.setData = function (data) { var groupChanged = data.group != undefined && this.data.group != data.group; if (groupChanged) { this.parent.itemSet._moveToGroup(this, data.group); } if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { this.editable = data.editable; } this.data = data; this.dirty = true; if (this.displayed) this.redraw(); }; /** * Set a parent for the item * @param {ItemSet | Group} parent */ Item.prototype.setParent = function (parent) { if (this.displayed) { this.hide(); this.parent = parent; if (this.parent) { this.show(); } } else { this.parent = parent; } }; /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ Item.prototype.isVisible = function (range) { // Should be implemented by Item implementations return false; }; /** * Show the Item in the DOM (when not already visible) * @return {Boolean} changed */ Item.prototype.show = function () { return false; }; /** * Hide the Item from the DOM (when visible) * @return {Boolean} changed */ Item.prototype.hide = function () { return false; }; /** * Repaint the item */ Item.prototype.redraw = function () {}; /** * Reposition the Item horizontally */ Item.prototype.repositionX = function () {}; /** * Reposition the Item vertically */ Item.prototype.repositionY = function () {}; /** * Repaint a delete button on the top right of the item when the item is selected * @param {HTMLElement} anchor * @protected */ Item.prototype._repaintDeleteButton = function (anchor) { var editable = (this.options.editable.remove || this.data.editable === true) && this.data.editable !== false; if (this.selected && editable && !this.dom.deleteButton) { // create and show button var me = this; var deleteButton = document.createElement('div'); deleteButton.className = 'vis-delete'; deleteButton.title = 'Delete this item'; // TODO: be able to destroy the delete button new Hammer(deleteButton).on('tap', function (event) { event.stopPropagation(); me.parent.removeFromDataSet(me); }); anchor.appendChild(deleteButton); this.dom.deleteButton = deleteButton; } else if (!this.selected && this.dom.deleteButton) { // remove button if (this.dom.deleteButton.parentNode) { this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); } this.dom.deleteButton = null; } }; /** * Set HTML contents for the item * @param {Element} element HTML element to fill with the contents * @private */ Item.prototype._updateContents = function (element) { var content; if (this.options.template) { var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset content = this.options.template(itemData); } else { content = this.data.content; } var changed = this._contentToString(this.content) !== this._contentToString(content); if (changed) { // only replace the content when changed if (content instanceof Element) { element.innerHTML = ''; element.appendChild(content); } else if (content != undefined) { element.innerHTML = content; } else { if (!(this.data.type == 'background' && this.data.content === undefined)) { throw new Error('Property "content" missing in item ' + this.id); } } this.content = content; } }; /** * Set HTML contents for the item * @param {Element} element HTML element to fill with the contents * @private */ Item.prototype._updateTitle = function (element) { if (this.data.title != null) { element.title = this.data.title || ''; } else { element.removeAttribute('vis-title'); } }; /** * Process dataAttributes timeline option and set as data- attributes on dom.content * @param {Element} element HTML element to which the attributes will be attached * @private */ Item.prototype._updateDataAttributes = function (element) { if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { var attributes = []; if (Array.isArray(this.options.dataAttributes)) { attributes = this.options.dataAttributes; } else if (this.options.dataAttributes == 'all') { attributes = Object.keys(this.data); } else { return; } for (var i = 0; i < attributes.length; i++) { var name = attributes[i]; var value = this.data[name]; if (value != null) { element.setAttribute('data-' + name, value); } else { element.removeAttribute('data-' + name); } } } }; /** * Update custom styles of the element * @param element * @private */ Item.prototype._updateStyle = function (element) { // remove old styles if (this.style) { util.removeCssText(element, this.style); this.style = null; } // append new styles if (this.data.style) { util.addCssText(element, this.data.style); this.style = this.data.style; } }; /** * Stringify the items contents * @param {string | Element | undefined} content * @returns {string | undefined} * @private */ Item.prototype._contentToString = function (content) { if (typeof content === 'string') return content; if (content && 'outerHTML' in content) return content.outerHTML; return content; }; /** * Return the width of the item left from its start date * @return {number} */ Item.prototype.getWidthLeft = function () { return 0; }; /** * Return the width of the item right from the max of its start and end date * @return {number} */ Item.prototype.getWidthRight = function () { return 0; }; module.exports = Item; // should be implemented by the item // should be implemented by the item // should be implemented by the item /***/ }, /* 36 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var moment = __webpack_require__(2); var DateUtil = __webpack_require__(29); var util = __webpack_require__(1); /** * @constructor TimeStep * The class TimeStep is an iterator for dates. You provide a start date and an * end date. The class itself determines the best scale (step size) based on the * provided start Date, end Date, and minimumStep. * * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * * Alternatively, you can set a scale by hand. * After creation, you can initialize the class by executing first(). Then you * can iterate from the start date to the end date via next(). You can check if * the end date is reached with the function hasNext(). After each step, you can * retrieve the current date via getCurrent(). * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, * days, to years. * * Version: 1.2 * * @param {Date} [start] The start date, for example new Date(2010, 9, 21) * or new Date(2010, 9, 21, 23, 45, 00) * @param {Date} [end] The end date * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ function TimeStep(start, end, minimumStep, hiddenDates) { // variables this.current = new Date(); this._start = new Date(); this._end = new Date(); this.autoScale = true; this.scale = 'day'; this.step = 1; // initialize the range this.setRange(start, end, minimumStep); // hidden Dates options this.switchedDay = false; this.switchedMonth = false; this.switchedYear = false; this.hiddenDates = hiddenDates; if (hiddenDates === undefined) { this.hiddenDates = []; } this.format = TimeStep.FORMAT; // default formatting } // Time formatting TimeStep.FORMAT = { minorLabels: { millisecond: 'SSS', second: 's', minute: 'HH:mm', hour: 'HH:mm', weekday: 'ddd D', day: 'D', month: 'MMM', year: 'YYYY' }, majorLabels: { millisecond: 'HH:mm:ss', second: 'D MMMM HH:mm', minute: 'ddd D MMMM', hour: 'ddd D MMMM', weekday: 'MMMM YYYY', day: 'MMMM YYYY', month: 'YYYY', year: '' } }; /** * Set custom formatting for the minor an major labels of the TimeStep. * Both `minorLabels` and `majorLabels` are an Object with properties: * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. * @param {{minorLabels: Object, majorLabels: Object}} format */ TimeStep.prototype.setFormat = function (format) { var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); this.format = util.deepExtend(defaultFormat, format); }; /** * Set a new range * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * @param {Date} [start] The start date and time. * @param {Date} [end] The end date and time. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep.prototype.setRange = function (start, end, minimumStep) { if (!(start instanceof Date) || !(end instanceof Date)) { throw 'No legal start or end date in method setRange'; } this._start = start != undefined ? new Date(start.valueOf()) : new Date(); this._end = end != undefined ? new Date(end.valueOf()) : new Date(); if (this.autoScale) { this.setMinimumStep(minimumStep); } }; /** * Set the range iterator to the start date. */ TimeStep.prototype.first = function () { this.current = new Date(this._start.valueOf()); this.roundToMinor(); }; /** * Round the current date to the first minor date value * This must be executed once when the current date is set to start Date */ TimeStep.prototype.roundToMinor = function () { // round to floor // IMPORTANT: we have no breaks in this switch! (this is no bug) // noinspection FallThroughInSwitchStatementJS switch (this.scale) { case 'year': this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); this.current.setMonth(0); case 'month': this.current.setDate(1); case 'day': // intentional fall through case 'weekday': this.current.setHours(0); case 'hour': this.current.setMinutes(0); case 'minute': this.current.setSeconds(0); case 'second': this.current.setMilliseconds(0); //case 'millisecond': // nothing to do for milliseconds } if (this.step != 1) { // round down to the first minor value that is a multiple of the current step size switch (this.scale) { case 'millisecond': this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);break; case 'second': this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);break; case 'minute': this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);break; case 'hour': this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);break; case 'weekday': // intentional fall through case 'day': this.current.setDate(this.current.getDate() - 1 - (this.current.getDate() - 1) % this.step + 1);break; case 'month': this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);break; case 'year': this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step);break; default: break; } } }; /** * Check if the there is a next step * @return {boolean} true if the current date has not passed the end date */ TimeStep.prototype.hasNext = function () { return this.current.valueOf() <= this._end.valueOf(); }; /** * Do the next step */ TimeStep.prototype.next = function () { var prev = this.current.valueOf(); // Two cases, needed to prevent issues with switching daylight savings // (end of March and end of October) if (this.current.getMonth() < 6) { switch (this.scale) { case 'millisecond': this.current = new Date(this.current.valueOf() + this.step);break; case 'second': this.current = new Date(this.current.valueOf() + this.step * 1000);break; case 'minute': this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; case 'hour': this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) var h = this.current.getHours(); this.current.setHours(h - h % this.step); break; case 'weekday': // intentional fall through case 'day': this.current.setDate(this.current.getDate() + this.step);break; case 'month': this.current.setMonth(this.current.getMonth() + this.step);break; case 'year': this.current.setFullYear(this.current.getFullYear() + this.step);break; default: break; } } else { switch (this.scale) { case 'millisecond': this.current = new Date(this.current.valueOf() + this.step);break; case 'second': this.current.setSeconds(this.current.getSeconds() + this.step);break; case 'minute': this.current.setMinutes(this.current.getMinutes() + this.step);break; case 'hour': this.current.setHours(this.current.getHours() + this.step);break; case 'weekday': // intentional fall through case 'day': this.current.setDate(this.current.getDate() + this.step);break; case 'month': this.current.setMonth(this.current.getMonth() + this.step);break; case 'year': this.current.setFullYear(this.current.getFullYear() + this.step);break; default: break; } } if (this.step != 1) { // round down to the correct major value switch (this.scale) { case 'millisecond': if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; case 'second': if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; case 'minute': if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; case 'hour': if (this.current.getHours() < this.step) this.current.setHours(0);break; case 'weekday': // intentional fall through case 'day': if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; case 'month': if (this.current.getMonth() < this.step) this.current.setMonth(0);break; case 'year': break; // nothing to do for year default: break; } } // safety mechanism: if current time is still unchanged, move to the end if (this.current.valueOf() == prev) { this.current = new Date(this._end.valueOf()); } DateUtil.stepOverHiddenDates(this, prev); }; /** * Get the current datetime * @return {Date} current The current date */ TimeStep.prototype.getCurrent = function () { return this.current; }; /** * Set a custom scale. Autoscaling will be disabled. * For example setScale('minute', 5) will result * in minor steps of 5 minutes, and major steps of an hour. * * @param {{scale: string, step: number}} params * An object containing two properties: * - A string 'scale'. Choose from 'millisecond', 'second', * 'minute', 'hour', 'weekday', 'day', 'month', 'year'. * - A number 'step'. A step size, by default 1. * Choose for example 1, 2, 5, or 10. */ TimeStep.prototype.setScale = function (params) { if (params && typeof params.scale == 'string') { this.scale = params.scale; this.step = params.step > 0 ? params.step : 1; this.autoScale = false; } }; /** * Enable or disable autoscaling * @param {boolean} enable If true, autoascaling is set true */ TimeStep.prototype.setAutoScale = function (enable) { this.autoScale = enable; }; /** * Automatically determine the scale that bests fits the provided minimum step * @param {Number} [minimumStep] The minimum step size in milliseconds */ TimeStep.prototype.setMinimumStep = function (minimumStep) { if (minimumStep == undefined) { return; } //var b = asc + ds; var stepYear = 1000 * 60 * 60 * 24 * 30 * 12; var stepMonth = 1000 * 60 * 60 * 24 * 30; var stepDay = 1000 * 60 * 60 * 24; var stepHour = 1000 * 60 * 60; var stepMinute = 1000 * 60; var stepSecond = 1000; var stepMillisecond = 1; // find the smallest step that is larger than the provided minimumStep if (stepYear * 1000 > minimumStep) { this.scale = 'year';this.step = 1000; } if (stepYear * 500 > minimumStep) { this.scale = 'year';this.step = 500; } if (stepYear * 100 > minimumStep) { this.scale = 'year';this.step = 100; } if (stepYear * 50 > minimumStep) { this.scale = 'year';this.step = 50; } if (stepYear * 10 > minimumStep) { this.scale = 'year';this.step = 10; } if (stepYear * 5 > minimumStep) { this.scale = 'year';this.step = 5; } if (stepYear > minimumStep) { this.scale = 'year';this.step = 1; } if (stepMonth * 3 > minimumStep) { this.scale = 'month';this.step = 3; } if (stepMonth > minimumStep) { this.scale = 'month';this.step = 1; } if (stepDay * 5 > minimumStep) { this.scale = 'day';this.step = 5; } if (stepDay * 2 > minimumStep) { this.scale = 'day';this.step = 2; } if (stepDay > minimumStep) { this.scale = 'day';this.step = 1; } if (stepDay / 2 > minimumStep) { this.scale = 'weekday';this.step = 1; } if (stepHour * 4 > minimumStep) { this.scale = 'hour';this.step = 4; } if (stepHour > minimumStep) { this.scale = 'hour';this.step = 1; } if (stepMinute * 15 > minimumStep) { this.scale = 'minute';this.step = 15; } if (stepMinute * 10 > minimumStep) { this.scale = 'minute';this.step = 10; } if (stepMinute * 5 > minimumStep) { this.scale = 'minute';this.step = 5; } if (stepMinute > minimumStep) { this.scale = 'minute';this.step = 1; } if (stepSecond * 15 > minimumStep) { this.scale = 'second';this.step = 15; } if (stepSecond * 10 > minimumStep) { this.scale = 'second';this.step = 10; } if (stepSecond * 5 > minimumStep) { this.scale = 'second';this.step = 5; } if (stepSecond > minimumStep) { this.scale = 'second';this.step = 1; } if (stepMillisecond * 200 > minimumStep) { this.scale = 'millisecond';this.step = 200; } if (stepMillisecond * 100 > minimumStep) { this.scale = 'millisecond';this.step = 100; } if (stepMillisecond * 50 > minimumStep) { this.scale = 'millisecond';this.step = 50; } if (stepMillisecond * 10 > minimumStep) { this.scale = 'millisecond';this.step = 10; } if (stepMillisecond * 5 > minimumStep) { this.scale = 'millisecond';this.step = 5; } if (stepMillisecond > minimumStep) { this.scale = 'millisecond';this.step = 1; } }; /** * Snap a date to a rounded value. * The snap intervals are dependent on the current scale and step. * Static function * @param {Date} date the date to be snapped. * @param {string} scale Current scale, can be 'millisecond', 'second', * 'minute', 'hour', 'weekday, 'day', 'month', 'year'. * @param {number} step Current step (1, 2, 4, 5, ... * @return {Date} snappedDate */ TimeStep.snap = function (date, scale, step) { var clone = new Date(date.valueOf()); if (scale == 'year') { var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); clone.setFullYear(Math.round(year / step) * step); clone.setMonth(0); clone.setDate(0); clone.setHours(0); clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); } else if (scale == 'month') { if (clone.getDate() > 15) { clone.setDate(1); clone.setMonth(clone.getMonth() + 1); // important: first set Date to 1, after that change the month. } else { clone.setDate(1); } clone.setHours(0); clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); } else if (scale == 'day') { //noinspection FallthroughInSwitchStatementJS switch (step) { case 5: case 2: clone.setHours(Math.round(clone.getHours() / 24) * 24);break; default: clone.setHours(Math.round(clone.getHours() / 12) * 12);break; } clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); } else if (scale == 'weekday') { //noinspection FallthroughInSwitchStatementJS switch (step) { case 5: case 2: clone.setHours(Math.round(clone.getHours() / 12) * 12);break; default: clone.setHours(Math.round(clone.getHours() / 6) * 6);break; } clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); } else if (scale == 'hour') { switch (step) { case 4: clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60);break; default: clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30);break; } clone.setSeconds(0); clone.setMilliseconds(0); } else if (scale == 'minute') { //noinspection FallthroughInSwitchStatementJS switch (step) { case 15: case 10: clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); clone.setSeconds(0); break; case 5: clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60);break; default: clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30);break; } clone.setMilliseconds(0); } else if (scale == 'second') { //noinspection FallthroughInSwitchStatementJS switch (step) { case 15: case 10: clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); clone.setMilliseconds(0); break; case 5: clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000);break; default: clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500);break; } } else if (scale == 'millisecond') { var _step = step > 5 ? step / 2 : 1; clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); } return clone; }; /** * Check if the current value is a major value (for example when the step * is DAY, a major value is each first day of the MONTH) * @return {boolean} true if current date is major, else false. */ TimeStep.prototype.isMajor = function () { if (this.switchedYear == true) { this.switchedYear = false; switch (this.scale) { case 'year': case 'month': case 'weekday': case 'day': case 'hour': case 'minute': case 'second': case 'millisecond': return true; default: return false; } } else if (this.switchedMonth == true) { this.switchedMonth = false; switch (this.scale) { case 'weekday': case 'day': case 'hour': case 'minute': case 'second': case 'millisecond': return true; default: return false; } } else if (this.switchedDay == true) { this.switchedDay = false; switch (this.scale) { case 'millisecond': case 'second': case 'minute': case 'hour': return true; default: return false; } } switch (this.scale) { case 'millisecond': return this.current.getMilliseconds() == 0; case 'second': return this.current.getSeconds() == 0; case 'minute': return this.current.getHours() == 0 && this.current.getMinutes() == 0; case 'hour': return this.current.getHours() == 0; case 'weekday': // intentional fall through case 'day': return this.current.getDate() == 1; case 'month': return this.current.getMonth() == 0; case 'year': return false; default: return false; } }; /** * Returns formatted text for the minor axislabel, depending on the current * date and the scale. For example when scale is MINUTE, the current time is * formatted as "hh:mm". * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMinor = function (date) { if (date == undefined) { date = this.current; } var format = this.format.minorLabels[this.scale]; return format && format.length > 0 ? moment(date).format(format) : ''; }; /** * Returns formatted text for the major axis label, depending on the current * date and the scale. For example when scale is MINUTE, the major scale is * hours, and the hour will be formatted as "hh". * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMajor = function (date) { if (date == undefined) { date = this.current; } var format = this.format.majorLabels[this.scale]; return format && format.length > 0 ? moment(date).format(format) : ''; }; TimeStep.prototype.getClassName = function () { var m = moment(this.current); var date = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function var step = this.step; function even(value) { return value / step % 2 == 0 ? ' vis-even' : ' vis-odd'; } function today(date) { if (date.isSame(new Date(), 'day')) { return ' vis-today'; } if (date.isSame(moment().add(1, 'day'), 'day')) { return ' vis-tomorrow'; } if (date.isSame(moment().add(-1, 'day'), 'day')) { return ' vis-yesterday'; } return ''; } function currentWeek(date) { return date.isSame(new Date(), 'week') ? ' vis-current-week' : ''; } function currentMonth(date) { return date.isSame(new Date(), 'month') ? ' vis-current-month' : ''; } function currentYear(date) { return date.isSame(new Date(), 'year') ? ' vis-current-year' : ''; } switch (this.scale) { case 'millisecond': return even(date.milliseconds()).trim(); case 'second': return even(date.seconds()).trim(); case 'minute': return even(date.minutes()).trim(); case 'hour': var hours = date.hours(); if (this.step == 4) { hours = hours + '-h' + (hours + 4); } return 'vis-h' + hours + today(date) + even(date.hours()); case 'weekday': return 'vis-' + date.format('dddd').toLowerCase() + today(date) + currentWeek(date) + even(date.date()); case 'day': var day = date.date(); var month = date.format('MMMM').toLowerCase(); return 'vis-day' + day + ' vis-' + month + currentMonth(date) + even(day - 1); case 'month': return 'vis-' + date.format('MMMM').toLowerCase() + currentMonth(date) + even(date.month()); case 'year': var year = date.year(); return 'vis-year' + year + currentYear(date) + even(year); default: return ''; } }; module.exports = TimeStep; /***/ }, /* 37 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var Group = __webpack_require__(32); /** * @constructor BackgroundGroup * @param {Number | String} groupId * @param {Object} data * @param {ItemSet} itemSet */ function BackgroundGroup(groupId, data, itemSet) { Group.call(this, groupId, data, itemSet); this.width = 0; this.height = 0; this.top = 0; this.left = 0; } BackgroundGroup.prototype = Object.create(Group.prototype); /** * Repaint this group * @param {{start: number, end: number}} range * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * @param {boolean} [restack=false] Force restacking of all items * @return {boolean} Returns true if the group is resized */ BackgroundGroup.prototype.redraw = function (range, margin, restack) { var resized = false; this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); // calculate actual size this.width = this.dom.background.offsetWidth; // apply new height (just always zero for BackgroundGroup this.dom.background.style.height = '0'; // update vertical position of items after they are re-stacked and the height of the group is calculated for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { var item = this.visibleItems[i]; item.repositionY(margin); } return resized; }; /** * Show this group: attach to the DOM */ BackgroundGroup.prototype.show = function () { if (!this.dom.background.parentNode) { this.itemSet.dom.background.appendChild(this.dom.background); } }; module.exports = BackgroundGroup; /***/ }, /* 38 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Item = __webpack_require__(35); var util = __webpack_require__(1); /** * @constructor BoxItem * @extends Item * @param {Object} data Object containing parameters start * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options * // TODO: describe available options */ function BoxItem(data, conversion, options) { this.props = { dot: { width: 0, height: 0 }, line: { width: 0, height: 0 } }; // validate data if (data) { if (data.start == undefined) { throw new Error('Property "start" missing in item ' + data); } } Item.call(this, data, conversion, options); } BoxItem.prototype = new Item(null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ BoxItem.prototype.isVisible = function (range) { // determine visibility // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; return this.data.start > range.start - interval && this.data.start < range.end + interval; }; /** * Repaint the item */ BoxItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; // create main box dom.box = document.createElement('DIV'); // contents box (inside the background box). used for making margins dom.content = document.createElement('DIV'); dom.content.className = 'vis-item-content'; dom.box.appendChild(dom.content); // line to axis dom.line = document.createElement('DIV'); dom.line.className = 'vis-line'; // dot on axis dom.dot = document.createElement('DIV'); dom.dot.className = 'vis-dot'; // attach this item as attribute dom.box['timeline-item'] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); foreground.appendChild(dom.box); } if (!dom.line.parentNode) { var background = this.parent.dom.background; if (!background) throw new Error('Cannot redraw item: parent has no background container element'); background.appendChild(dom.line); } if (!dom.dot.parentNode) { var axis = this.parent.dom.axis; if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); axis.appendChild(dom.dot); } this.displayed = true; // Update DOM when item is marked dirty. An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); this._updateTitle(this.dom.box); this._updateDataAttributes(this.dom.box); this._updateStyle(this.dom.box); var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; // update class var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); dom.box.className = 'vis-item vis-box' + className; dom.line.className = 'vis-item vis-line' + className; dom.dot.className = 'vis-item vis-dot' + className; // recalculate size this.props.dot.height = dom.dot.offsetHeight; this.props.dot.width = dom.dot.offsetWidth; this.props.line.width = dom.line.offsetWidth; this.width = dom.box.offsetWidth; this.height = dom.box.offsetHeight; this.dirty = false; } this._repaintDeleteButton(dom.box); }; /** * Show the item in the DOM (when not already displayed). The items DOM will * be created when needed. */ BoxItem.prototype.show = function () { if (!this.displayed) { this.redraw(); } }; /** * Hide the item from the DOM (when visible) */ BoxItem.prototype.hide = function () { if (this.displayed) { var dom = this.dom; if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); this.displayed = false; } }; /** * Reposition the item horizontally * @Override */ BoxItem.prototype.repositionX = function () { var start = this.conversion.toScreen(this.data.start); var align = this.options.align; var left; // calculate left position of the box if (align == 'right') { this.left = start - this.width; } else if (align == 'left') { this.left = start; } else { // default or 'center' this.left = start - this.width / 2; } // reposition box this.dom.box.style.left = this.left + 'px'; // reposition line this.dom.line.style.left = start - this.props.line.width / 2 + 'px'; // reposition dot this.dom.dot.style.left = start - this.props.dot.width / 2 + 'px'; }; /** * Reposition the item vertically * @Override */ BoxItem.prototype.repositionY = function () { var orientation = this.options.orientation.item; var box = this.dom.box; var line = this.dom.line; var dot = this.dom.dot; if (orientation == 'top') { box.style.top = (this.top || 0) + 'px'; line.style.top = '0'; line.style.height = this.parent.top + this.top + 1 + 'px'; line.style.bottom = ''; } else { // orientation 'bottom' var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; line.style.top = itemSetHeight - lineHeight + 'px'; line.style.bottom = '0'; } dot.style.top = -this.props.dot.height / 2 + 'px'; }; /** * Return the width of the item left from its start date * @return {number} */ BoxItem.prototype.getWidthLeft = function () { return this.width / 2; }; /** * Return the width of the item right from its start date * @return {number} */ BoxItem.prototype.getWidthRight = function () { return this.width / 2; }; module.exports = BoxItem; /***/ }, /* 39 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Item = __webpack_require__(35); /** * @constructor PointItem * @extends Item * @param {Object} data Object containing parameters start * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options * // TODO: describe available options */ function PointItem(data, conversion, options) { this.props = { dot: { top: 0, width: 0, height: 0 }, content: { height: 0, marginLeft: 0 } }; // validate data if (data) { if (data.start == undefined) { throw new Error('Property "start" missing in item ' + data); } } Item.call(this, data, conversion, options); } PointItem.prototype = new Item(null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ PointItem.prototype.isVisible = function (range) { // determine visibility // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; return this.data.start > range.start - interval && this.data.start < range.end + interval; }; /** * Repaint the item */ PointItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; // background box dom.point = document.createElement('div'); // className is updated in redraw() // contents box, right from the dot dom.content = document.createElement('div'); dom.content.className = 'vis-item-content'; dom.point.appendChild(dom.content); // dot at start dom.dot = document.createElement('div'); dom.point.appendChild(dom.dot); // attach this item as attribute dom.point['timeline-item'] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } if (!dom.point.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { throw new Error('Cannot redraw item: parent has no foreground container element'); } foreground.appendChild(dom.point); } this.displayed = true; // Update DOM when item is marked dirty. An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); this._updateTitle(this.dom.point); this._updateDataAttributes(this.dom.point); this._updateStyle(this.dom.point); var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; // update class var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); dom.point.className = 'vis-item vis-point' + className; dom.dot.className = 'vis-item vis-dot' + className; // recalculate size of dot and contents this.props.dot.width = dom.dot.offsetWidth; this.props.dot.height = dom.dot.offsetHeight; this.props.content.height = dom.content.offsetHeight; // resize contents dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; //dom.content.style.marginRight = ... + 'px'; // TODO: margin right dom.dot.style.top = (this.height - this.props.dot.height) / 2 + 'px'; dom.dot.style.left = this.props.dot.width / 2 + 'px'; // recalculate size this.width = dom.point.offsetWidth; this.height = dom.point.offsetHeight; this.dirty = false; } this._repaintDeleteButton(dom.point); }; /** * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ PointItem.prototype.show = function () { if (!this.displayed) { this.redraw(); } }; /** * Hide the item from the DOM (when visible) */ PointItem.prototype.hide = function () { if (this.displayed) { if (this.dom.point.parentNode) { this.dom.point.parentNode.removeChild(this.dom.point); } this.displayed = false; } }; /** * Reposition the item horizontally * @Override */ PointItem.prototype.repositionX = function () { var start = this.conversion.toScreen(this.data.start); this.left = start - this.props.dot.width; // reposition point this.dom.point.style.left = this.left + 'px'; }; /** * Reposition the item vertically * @Override */ PointItem.prototype.repositionY = function () { var orientation = this.options.orientation.item; var point = this.dom.point; if (orientation == 'top') { point.style.top = this.top + 'px'; } else { point.style.top = this.parent.height - this.top - this.height + 'px'; } }; /** * Return the width of the item left from its start date * @return {number} */ PointItem.prototype.getWidthLeft = function () { return this.props.dot.width; }; /** * Return the width of the item right from its start date * @return {number} */ PointItem.prototype.getWidthRight = function () { return this.width - this.props.dot.width; }; module.exports = PointItem; /***/ }, /* 40 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Hammer = __webpack_require__(23); var Item = __webpack_require__(35); var BackgroundGroup = __webpack_require__(37); var RangeItem = __webpack_require__(34); /** * @constructor BackgroundItem * @extends Item * @param {Object} data Object containing parameters start, end * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options * // TODO: describe options */ // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation function BackgroundItem(data, conversion, options) { this.props = { content: { width: 0 } }; this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { if (data.start == undefined) { throw new Error('Property "start" missing in item ' + data.id); } if (data.end == undefined) { throw new Error('Property "end" missing in item ' + data.id); } } Item.call(this, data, conversion, options); } BackgroundItem.prototype = new Item(null, null, null); BackgroundItem.prototype.baseClassName = 'vis-item vis-background'; BackgroundItem.prototype.stack = false; /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ BackgroundItem.prototype.isVisible = function (range) { // determine visibility return this.data.start < range.end && this.data.end > range.start; }; /** * Repaint the item */ BackgroundItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; // background box dom.box = document.createElement('div'); // className is updated in redraw() // frame box (to prevent the item contents from overflowing dom.frame = document.createElement('div'); dom.frame.className = 'vis-item-overflow'; dom.box.appendChild(dom.frame); // contents box dom.content = document.createElement('div'); dom.content.className = 'vis-item-content'; dom.frame.appendChild(dom.content); // Note: we do NOT attach this item as attribute to the DOM, // such that background items cannot be selected //dom.box['timeline-item'] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } if (!dom.box.parentNode) { var background = this.parent.dom.background; if (!background) { throw new Error('Cannot redraw item: parent has no background container element'); } background.appendChild(dom.box); } this.displayed = true; // Update DOM when item is marked dirty. An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); this._updateTitle(this.dom.content); this._updateDataAttributes(this.dom.content); this._updateStyle(this.dom.box); // update class var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : ''); dom.box.className = this.baseClassName + className; // determine from css whether this box has overflow this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; // recalculate size this.props.content.width = this.dom.content.offsetWidth; this.height = 0; // set height zero, so this item will be ignored when stacking items this.dirty = false; } }; /** * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ BackgroundItem.prototype.show = RangeItem.prototype.show; /** * Hide the item from the DOM (when visible) * @return {Boolean} changed */ BackgroundItem.prototype.hide = RangeItem.prototype.hide; /** * Reposition the item horizontally * @Override */ BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; /** * Reposition the item vertically * @Override */ BackgroundItem.prototype.repositionY = function (margin) { var onTop = this.options.orientation.item === 'top'; this.dom.content.style.top = onTop ? '' : '0'; this.dom.content.style.bottom = onTop ? '0' : ''; var height; // special positioning for subgroups if (this.data.subgroup !== undefined) { // TODO: instead of calculating the top position of the subgroups here for every BackgroundItem, calculate the top of the subgroup once in Itemset var itemSubgroup = this.data.subgroup; var subgroups = this.parent.subgroups; var subgroupIndex = subgroups[itemSubgroup].index; // if the orientation is top, we need to take the difference in height into account. if (onTop == true) { // the first subgroup will have to account for the distance from the top to the first item. height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; height += subgroupIndex == 0 ? margin.axis - 0.5 * margin.item.vertical : 0; var newTop = this.parent.top; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroupIndex) { newTop += subgroups[subgroup].height + margin.item.vertical; } } } // the others will have to be offset downwards with this same distance. newTop += subgroupIndex != 0 ? margin.axis - 0.5 * margin.item.vertical : 0; this.dom.box.style.top = newTop + 'px'; this.dom.box.style.bottom = ''; } // and when the orientation is bottom: else { var newTop = this.parent.top; var totalHeight = 0; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { if (subgroups[subgroup].visible == true) { var newHeight = subgroups[subgroup].height + margin.item.vertical; totalHeight += newHeight; if (subgroups[subgroup].index > subgroupIndex) { newTop += newHeight; } } } } height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; this.dom.box.style.top = this.parent.height - totalHeight + newTop + 'px'; this.dom.box.style.bottom = ''; } } // and in the case of no subgroups: else { // we want backgrounds with groups to only show in groups. if (this.parent instanceof BackgroundGroup) { // if the item is not in a group: height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); this.dom.box.style.top = onTop ? '0' : ''; this.dom.box.style.bottom = onTop ? '' : '0'; } else { height = this.parent.height; // same alignment for items when orientation is top or bottom this.dom.box.style.top = this.parent.top + 'px'; this.dom.box.style.bottom = ''; } } this.dom.box.style.height = height + 'px'; }; module.exports = BackgroundItem; /***/ }, /* 41 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var Component = __webpack_require__(21); var TimeStep = __webpack_require__(36); var DateUtil = __webpack_require__(29); var moment = __webpack_require__(2); /** * A horizontal time axis * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body * @param {Object} [options] See TimeAxis.setOptions for the available * options. * @constructor TimeAxis * @extends Component */ function TimeAxis(body, options) { this.dom = { foreground: null, lines: [], majorTexts: [], minorTexts: [], redundant: { lines: [], majorTexts: [], minorTexts: [] } }; this.props = { range: { start: 0, end: 0, minimumStep: 0 }, lineTop: 0 }; this.defaultOptions = { orientation: { axis: 'bottom' }, // axis orientation: 'top' or 'bottom' showMinorLabels: true, showMajorLabels: true, format: TimeStep.FORMAT, timeAxis: null }; this.options = util.extend({}, this.defaultOptions); this.body = body; // create the HTML DOM this._create(); this.setOptions(options); } TimeAxis.prototype = new Component(); /** * Set options for the TimeAxis. * Parameters will be merged in current options. * @param {Object} options Available options: * {string} [orientation.axis] * {boolean} [showMinorLabels] * {boolean} [showMajorLabels] */ TimeAxis.prototype.setOptions = function (options) { if (options) { // copy all options that we know util.selectiveExtend(['showMinorLabels', 'showMajorLabels', 'hiddenDates', 'timeAxis'], this.options, options); // deep copy the format options util.selectiveDeepExtend(['format'], this.options, options); if ('orientation' in options) { if (typeof options.orientation === 'string') { this.options.orientation.axis = options.orientation; } else if (typeof options.orientation === 'object' && 'axis' in options.orientation) { this.options.orientation.axis = options.orientation.axis; } } // apply locale to moment.js // TODO: not so nice, this is applied globally to moment.js if ('locale' in options) { if (typeof moment.locale === 'function') { // moment.js 2.8.1+ moment.locale(options.locale); } else { moment.lang(options.locale); } } } }; /** * Create the HTML DOM for the TimeAxis */ TimeAxis.prototype._create = function () { this.dom.foreground = document.createElement('div'); this.dom.background = document.createElement('div'); this.dom.foreground.className = 'vis-time-axis vis-foreground'; this.dom.background.className = 'vis-time-axis vis-background'; }; /** * Destroy the TimeAxis */ TimeAxis.prototype.destroy = function () { // remove from DOM if (this.dom.foreground.parentNode) { this.dom.foreground.parentNode.removeChild(this.dom.foreground); } if (this.dom.background.parentNode) { this.dom.background.parentNode.removeChild(this.dom.background); } this.body = null; }; /** * Repaint the component * @return {boolean} Returns true if the component is resized */ TimeAxis.prototype.redraw = function () { var props = this.props; var foreground = this.dom.foreground; var background = this.dom.background; // determine the correct parent DOM element (depending on option orientation) var parent = this.options.orientation.axis == 'top' ? this.body.dom.top : this.body.dom.bottom; var parentChanged = foreground.parentNode !== parent; // calculate character width and height this._calculateCharSize(); // TODO: recalculate sizes only needed when parent is resized or options is changed var showMinorLabels = this.options.showMinorLabels && this.options.orientation.axis !== 'none'; var showMajorLabels = this.options.showMajorLabels && this.options.orientation.axis !== 'none'; // determine the width and height of the elemens for the axis props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; props.height = props.minorLabelHeight + props.majorLabelHeight; props.width = foreground.offsetWidth; props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - (this.options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height); props.minorLineWidth = 1; // TODO: really calculate width props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight; props.majorLineWidth = 1; // TODO: really calculate width // take foreground and background offline while updating (is almost twice as fast) var foregroundNextSibling = foreground.nextSibling; var backgroundNextSibling = background.nextSibling; foreground.parentNode && foreground.parentNode.removeChild(foreground); background.parentNode && background.parentNode.removeChild(background); foreground.style.height = this.props.height + 'px'; this._repaintLabels(); // put DOM online again (at the same place) if (foregroundNextSibling) { parent.insertBefore(foreground, foregroundNextSibling); } else { parent.appendChild(foreground); } if (backgroundNextSibling) { this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling); } else { this.body.dom.backgroundVertical.appendChild(background); } return this._isResized() || parentChanged; }; /** * Repaint major and minor text labels and vertical grid lines * @private */ TimeAxis.prototype._repaintLabels = function () { var orientation = this.options.orientation.axis; // calculate range and step (step such that we have space for 7 characters per label) var start = util.convert(this.body.range.start, 'Number'); var end = util.convert(this.body.range.end, 'Number'); var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf(); var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize); minimumStep -= this.body.util.toTime(0).valueOf(); var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates); if (this.options.format) { step.setFormat(this.options.format); } if (this.options.timeAxis) { step.setScale(this.options.timeAxis); } this.step = step; // Move all DOM elements to a "redundant" list, where they // can be picked for re-use, and clear the lists with lines and texts. // At the end of the function _repaintLabels, left over elements will be cleaned up var dom = this.dom; dom.redundant.lines = dom.lines; dom.redundant.majorTexts = dom.majorTexts; dom.redundant.minorTexts = dom.minorTexts; dom.lines = []; dom.majorTexts = []; dom.minorTexts = []; var cur; var x = 0; var isMajor; var xPrev = 0; var width = 0; var prevLine; var xFirstMajorLabel = undefined; var max = 0; var className; step.first(); while (step.hasNext() && max < 1000) { max++; cur = step.getCurrent(); isMajor = step.isMajor(); className = step.getClassName(); xPrev = x; x = this.body.util.toScreen(cur); width = x - xPrev; if (prevLine) { prevLine.style.width = width + 'px'; } if (this.options.showMinorLabels) { this._repaintMinorText(x, step.getLabelMinor(), orientation, className); } if (isMajor && this.options.showMajorLabels) { if (x > 0) { if (xFirstMajorLabel == undefined) { xFirstMajorLabel = x; } this._repaintMajorText(x, step.getLabelMajor(), orientation, className); } prevLine = this._repaintMajorLine(x, orientation, className); } else { prevLine = this._repaintMinorLine(x, orientation, className); } step.next(); } // create a major label on the left when needed if (this.options.showMajorLabels) { var leftTime = this.body.util.toTime(0), leftText = step.getLabelMajor(leftTime), widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { this._repaintMajorText(0, leftText, orientation, className); } } // Cleanup leftover DOM elements from the redundant list util.forEach(this.dom.redundant, function (arr) { while (arr.length) { var elem = arr.pop(); if (elem && elem.parentNode) { elem.parentNode.removeChild(elem); } } }); }; /** * Create a minor label for the axis at position x * @param {Number} x * @param {String} text * @param {String} orientation "top" or "bottom" (default) * @param {String} className * @private */ TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { // reuse redundant label var label = this.dom.redundant.minorTexts.shift(); if (!label) { // create new label var content = document.createTextNode(''); label = document.createElement('div'); label.appendChild(content); this.dom.foreground.appendChild(label); } this.dom.minorTexts.push(label); label.childNodes[0].nodeValue = text; label.style.top = orientation == 'top' ? this.props.majorLabelHeight + 'px' : '0'; label.style.left = x + 'px'; label.className = 'vis-text vis-minor ' + className; //label.title = title; // TODO: this is a heavy operation }; /** * Create a Major label for the axis at position x * @param {Number} x * @param {String} text * @param {String} orientation "top" or "bottom" (default) * @param {String} className * @private */ TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { // reuse redundant label var label = this.dom.redundant.majorTexts.shift(); if (!label) { // create label var content = document.createTextNode(text); label = document.createElement('div'); label.appendChild(content); this.dom.foreground.appendChild(label); } this.dom.majorTexts.push(label); label.childNodes[0].nodeValue = text; label.className = 'vis-text vis-major ' + className; //label.title = title; // TODO: this is a heavy operation label.style.top = orientation == 'top' ? '0' : this.props.minorLabelHeight + 'px'; label.style.left = x + 'px'; }; /** * Create a minor line for the axis at position x * @param {Number} x * @param {String} orientation "top" or "bottom" (default) * @param {String} className * @return {Element} Returns the created line * @private */ TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { // reuse redundant line var line = this.dom.redundant.lines.shift(); if (!line) { // create vertical line line = document.createElement('div'); this.dom.background.appendChild(line); } this.dom.lines.push(line); var props = this.props; if (orientation == 'top') { line.style.top = props.majorLabelHeight + 'px'; } else { line.style.top = this.body.domProps.top.height + 'px'; } line.style.height = props.minorLineHeight + 'px'; line.style.left = x - props.minorLineWidth / 2 + 'px'; line.className = 'vis-grid vis-vertical vis-minor ' + className; return line; }; /** * Create a Major line for the axis at position x * @param {Number} x * @param {String} orientation "top" or "bottom" (default) * @param {String} className * @return {Element} Returns the created line * @private */ TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) { // reuse redundant line var line = this.dom.redundant.lines.shift(); if (!line) { // create vertical line line = document.createElement('div'); this.dom.background.appendChild(line); } this.dom.lines.push(line); var props = this.props; if (orientation == 'top') { line.style.top = '0'; } else { line.style.top = this.body.domProps.top.height + 'px'; } line.style.left = x - props.majorLineWidth / 2 + 'px'; line.style.height = props.majorLineHeight + 'px'; line.className = 'vis-grid vis-vertical vis-major ' + className; return line; }; /** * Determine the size of text on the axis (both major and minor axis). * The size is calculated only once and then cached in this.props. * @private */ TimeAxis.prototype._calculateCharSize = function () { // Note: We calculate char size with every redraw. Size may change, for // example when any of the timelines parents had display:none for example. // determine the char width and height on the minor axis if (!this.dom.measureCharMinor) { this.dom.measureCharMinor = document.createElement('DIV'); this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure'; this.dom.measureCharMinor.style.position = 'absolute'; this.dom.measureCharMinor.appendChild(document.createTextNode('0')); this.dom.foreground.appendChild(this.dom.measureCharMinor); } this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; // determine the char width and height on the major axis if (!this.dom.measureCharMajor) { this.dom.measureCharMajor = document.createElement('DIV'); this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure'; this.dom.measureCharMajor.style.position = 'absolute'; this.dom.measureCharMajor.appendChild(document.createTextNode('0')); this.dom.foreground.appendChild(this.dom.measureCharMajor); } this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; }; module.exports = TimeAxis; /***/ }, /* 42 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var keycharm = __webpack_require__(43); var Emitter = __webpack_require__(13); var Hammer = __webpack_require__(23); var util = __webpack_require__(1); /** * Turn an element into an clickToUse element. * When not active, the element has a transparent overlay. When the overlay is * clicked, the mode is changed to active. * When active, the element is displayed with a blue border around it, and * the interactive contents of the element can be used. When clicked outside * the element, the elements mode is changed to inactive. * @param {Element} container * @constructor */ function Activator(container) { this.active = false; this.dom = { container: container }; this.dom.overlay = document.createElement('div'); this.dom.overlay.className = 'vis-overlay'; this.dom.container.appendChild(this.dom.overlay); this.hammer = Hammer(this.dom.overlay); this.hammer.on('tap', this._onTapOverlay.bind(this)); // block all touch events (except tap) var me = this; var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend']; events.forEach(function (event) { me.hammer.on(event, function (event) { event.stopPropagation(); }); }); // attach a click event to the window, in order to deactivate when clicking outside the timeline if (document && document.body) { this.onClick = function (event) { if (!_hasParent(event.target, container)) { me.deactivate(); } }; document.body.addEventListener('click', this.onClick); } if (this.keycharm !== undefined) { this.keycharm.destroy(); } this.keycharm = keycharm(); // keycharm listener only bounded when active) this.escListener = this.deactivate.bind(this); } // turn into an event emitter Emitter(Activator.prototype); // The currently active activator Activator.current = null; /** * Destroy the activator. Cleans up all created DOM and event listeners */ Activator.prototype.destroy = function () { this.deactivate(); // remove dom this.dom.overlay.parentNode.removeChild(this.dom.overlay); // remove global event listener if (this.onClick) { document.body.removeEventListener('click', this.onClick); } // cleanup hammer instances this.hammer.destroy(); this.hammer = null; // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) }; /** * Activate the element * Overlay is hidden, element is decorated with a blue shadow border */ Activator.prototype.activate = function () { // we allow only one active activator at a time if (Activator.current) { Activator.current.deactivate(); } Activator.current = this; this.active = true; this.dom.overlay.style.display = 'none'; util.addClassName(this.dom.container, 'vis-active'); this.emit('change'); this.emit('activate'); // ugly hack: bind ESC after emitting the events, as the Network rebinds all // keyboard events on a 'change' event this.keycharm.bind('esc', this.escListener); }; /** * Deactivate the element * Overlay is displayed on top of the element */ Activator.prototype.deactivate = function () { this.active = false; this.dom.overlay.style.display = ''; util.removeClassName(this.dom.container, 'vis-active'); this.keycharm.unbind('esc', this.escListener); this.emit('change'); this.emit('deactivate'); }; /** * Handle a tap event: activate the container * @param event * @private */ Activator.prototype._onTapOverlay = function (event) { // activate the container this.activate(); event.stopPropagation(); }; /** * Test whether the element has the requested parent element somewhere in * its chain of parent nodes. * @param {HTMLElement} element * @param {HTMLElement} parent * @returns {boolean} Returns true when the parent is found somewhere in the * chain of parent nodes. * @private */ function _hasParent(element, parent) { while (element) { if (element === parent) { return true; } element = element.parentNode; } return false; } module.exports = Activator; /***/ }, /* 43 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; /** * Created by Alex on 11/6/2014. */ // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 // if the module has no dependencies, the above pattern can be simplified to (function (root, factory) { if (true) { // AMD. Register as an anonymous module. !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.keycharm = factory(); } }(this, function () { function keycharm(options) { var preventDefault = options && options.preventDefault || false; var container = options && options.container || window; var _exportFunctions = {}; var _bound = {keydown:{}, keyup:{}}; var _keys = {}; var i; // a - z for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} // A - Z for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} // 0 - 9 for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} // F1 - F12 for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} // num0 - num9 for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} // numpad misc _keys['num*'] = {code:106, shift: false}; _keys['num+'] = {code:107, shift: false}; _keys['num-'] = {code:109, shift: false}; _keys['num/'] = {code:111, shift: false}; _keys['num.'] = {code:110, shift: false}; // arrows _keys['left'] = {code:37, shift: false}; _keys['up'] = {code:38, shift: false}; _keys['right'] = {code:39, shift: false}; _keys['down'] = {code:40, shift: false}; // extra keys _keys['space'] = {code:32, shift: false}; _keys['enter'] = {code:13, shift: false}; _keys['shift'] = {code:16, shift: undefined}; _keys['esc'] = {code:27, shift: false}; _keys['backspace'] = {code:8, shift: false}; _keys['tab'] = {code:9, shift: false}; _keys['ctrl'] = {code:17, shift: false}; _keys['alt'] = {code:18, shift: false}; _keys['delete'] = {code:46, shift: false}; _keys['pageup'] = {code:33, shift: false}; _keys['pagedown'] = {code:34, shift: false}; // symbols _keys['='] = {code:187, shift: false}; _keys['-'] = {code:189, shift: false}; _keys[']'] = {code:221, shift: false}; _keys['['] = {code:219, shift: false}; var down = function(event) {handleEvent(event,'keydown');}; var up = function(event) {handleEvent(event,'keyup');}; // handle the actualy bound key with the event var handleEvent = function(event,type) { if (_bound[type][event.keyCode] !== undefined) { var bound = _bound[type][event.keyCode]; for (var i = 0; i < bound.length; i++) { if (bound[i].shift === undefined) { bound[i].fn(event); } else if (bound[i].shift == true && event.shiftKey == true) { bound[i].fn(event); } else if (bound[i].shift == false && event.shiftKey == false) { bound[i].fn(event); } } if (preventDefault == true) { event.preventDefault(); } } }; // bind a key to a callback _exportFunctions.bind = function(key, callback, type) { if (type === undefined) { type = 'keydown'; } if (_keys[key] === undefined) { throw new Error("unsupported key: " + key); } if (_bound[type][_keys[key].code] === undefined) { _bound[type][_keys[key].code] = []; } _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); }; // bind all keys to a call back (demo purposes) _exportFunctions.bindAll = function(callback, type) { if (type === undefined) { type = 'keydown'; } for (var key in _keys) { if (_keys.hasOwnProperty(key)) { _exportFunctions.bind(key,callback,type); } } }; // get the key label from an event _exportFunctions.getKey = function(event) { for (var key in _keys) { if (_keys.hasOwnProperty(key)) { if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { return key; } else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { return key; } else if (event.keyCode == _keys[key].code && key == 'shift') { return key; } } } return "unknown key, currently not supported"; }; // unbind either a specific callback from a key or all of them (by leaving callback undefined) _exportFunctions.unbind = function(key, callback, type) { if (type === undefined) { type = 'keydown'; } if (_keys[key] === undefined) { throw new Error("unsupported key: " + key); } if (callback !== undefined) { var newBindings = []; var bound = _bound[type][_keys[key].code]; if (bound !== undefined) { for (var i = 0; i < bound.length; i++) { if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { newBindings.push(_bound[type][_keys[key].code][i]); } } } _bound[type][_keys[key].code] = newBindings; } else { _bound[type][_keys[key].code] = []; } }; // reset all bound variables. _exportFunctions.reset = function() { _bound = {keydown:{}, keyup:{}}; }; // unbind all listeners and reset all variables. _exportFunctions.destroy = function() { _bound = {keydown:{}, keyup:{}}; container.removeEventListener('keydown', down, true); container.removeEventListener('keyup', up, true); }; // create listeners. container.addEventListener('keydown',down,true); container.addEventListener('keyup',up,true); // return the public functions. return _exportFunctions; } return keycharm; })); /***/ }, /* 44 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Hammer = __webpack_require__(23); var util = __webpack_require__(1); var Component = __webpack_require__(21); var moment = __webpack_require__(2); var locales = __webpack_require__(22); /** * A custom time bar * @param {{range: Range, dom: Object}} body * @param {Object} [options] Available parameters: * {number | string} id * {string} locales * {string} locale * @constructor CustomTime * @extends Component */ function CustomTime(body, options) { this.body = body; // default options this.defaultOptions = { locales: locales, locale: 'en', id: undefined }; this.options = util.extend({}, this.defaultOptions); if (options && options.time) { this.customTime = options.time; } else { this.customTime = new Date(); } this.eventParams = {}; // stores state parameters while dragging the bar this.setOptions(options); // create the DOM this._create(); } CustomTime.prototype = new Component(); /** * Set options for the component. Options will be merged in current options. * @param {Object} options Available parameters: * {number | string} id * {string} locales * {string} locale */ CustomTime.prototype.setOptions = function (options) { if (options) { // copy all options that we know util.selectiveExtend(['locale', 'locales', 'id'], this.options, options); } }; /** * Create the DOM for the custom time * @private */ CustomTime.prototype._create = function () { var bar = document.createElement('div'); bar['custom-time'] = this; bar.className = 'vis-custom-time ' + (this.options.id || ''); bar.style.position = 'absolute'; bar.style.top = '0px'; bar.style.height = '100%'; this.bar = bar; var drag = document.createElement('div'); drag.style.position = 'relative'; drag.style.top = '0px'; drag.style.left = '-10px'; drag.style.height = '100%'; drag.style.width = '20px'; bar.appendChild(drag); // attach event listeners this.hammer = new Hammer(drag); this.hammer.on('panstart', this._onDragStart.bind(this)); this.hammer.on('panmove', this._onDrag.bind(this)); this.hammer.on('panend', this._onDragEnd.bind(this)); // TODO: cleanup //this.hammer.on('pan', function (event) { // event.preventDefault(); //}); }; /** * Destroy the CustomTime bar */ CustomTime.prototype.destroy = function () { this.hide(); this.hammer.destroy(); this.hammer = null; this.body = null; }; /** * Repaint the component * @return {boolean} Returns true if the component is resized */ CustomTime.prototype.redraw = function () { var parent = this.body.dom.backgroundVertical; if (this.bar.parentNode != parent) { // attach to the dom if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); } parent.appendChild(this.bar); } var x = this.body.util.toScreen(this.customTime); var locale = this.options.locales[this.options.locale]; if (!locale) { if (!this.warned) { console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); this.warned = true; } locale = this.options.locales['en']; // fall back on english when not available } var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); title = title.charAt(0).toUpperCase() + title.substring(1); this.bar.style.left = x + 'px'; this.bar.title = title; return false; }; /** * Remove the CustomTime from the DOM */ CustomTime.prototype.hide = function () { // remove the line from the DOM if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); } }; /** * Set custom time. * @param {Date | number | string} time */ CustomTime.prototype.setCustomTime = function (time) { this.customTime = util.convert(time, 'Date'); this.redraw(); }; /** * Retrieve the current custom time. * @return {Date} customTime */ CustomTime.prototype.getCustomTime = function () { return new Date(this.customTime.valueOf()); }; /** * Start moving horizontally * @param {Event} event * @private */ CustomTime.prototype._onDragStart = function (event) { this.eventParams.dragging = true; this.eventParams.customTime = this.customTime; event.stopPropagation(); }; /** * Perform moving operating. * @param {Event} event * @private */ CustomTime.prototype._onDrag = function (event) { if (!this.eventParams.dragging) return; var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; var time = this.body.util.toTime(x); this.setCustomTime(time); // fire a timechange event this.body.emitter.emit('timechange', { id: this.options.id, time: new Date(this.customTime.valueOf()) }); event.stopPropagation(); }; /** * Stop moving operating. * @param {Event} event * @private */ CustomTime.prototype._onDragEnd = function (event) { if (!this.eventParams.dragging) return; // fire a timechanged event this.body.emitter.emit('timechanged', { id: this.options.id, time: new Date(this.customTime.valueOf()) }); event.stopPropagation(); }; /** * Find a custom time from an event target: * searches for the attribute 'custom-time' in the event target's element tree * @param {Event} event * @return {CustomTime | null} customTime */ CustomTime.customTimeFromTarget = function (event) { var target = event.target; while (target) { if (target.hasOwnProperty('custom-time')) { return target['custom-time']; } target = target.parentNode; } return null; }; module.exports = CustomTime; /***/ }, /* 45 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _ColorPicker = __webpack_require__(46); var _ColorPicker2 = _interopRequireDefault(_ColorPicker); var util = __webpack_require__(1); /** * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. * Boolean options are recognised as Boolean * Number options should be written as array: [default value, min value, max value, stepsize] * Colors should be written as array: ['color', '#ffffff'] * Strings with should be written as array: [option1, option2, option3, ..] * * The options are matched with their counterparts in each of the modules and the values used in the configuration are * * @param parentModule | the location where parentModule.setOptions() can be called * @param defaultContainer | the default container of the module * @param configureOptions | the fully configured and predefined options set found in allOptions.js * @param pixelRatio | canvas pixel ratio */ var Configurator = (function () { function Configurator(parentModule, defaultContainer, configureOptions) { var pixelRatio = arguments[3] === undefined ? 1 : arguments[3]; _classCallCheck(this, Configurator); this.parent = parentModule; this.changedOptions = []; this.container = defaultContainer; this.allowCreation = false; this.options = {}; this.defaultOptions = { enabled: false, filter: true, container: undefined, showButton: true }; util.extend(this.options, this.defaultOptions); this.configureOptions = configureOptions; this.moduleOptions = {}; this.domElements = []; this.colorPicker = new _ColorPicker2['default'](pixelRatio); this.wrapper = undefined; } _createClass(Configurator, [{ key: 'setOptions', /** * refresh all options. * Because all modules parse their options by themselves, we just use their options. We copy them here. * * @param options */ value: function setOptions(options) { if (options !== undefined) { var enabled = true; if (typeof options === 'string') { this.options.filter = options; } else if (options instanceof Array) { this.options.filter = options.join(); } else if (typeof options === 'object') { if (options.container !== undefined) { this.options.container = options.container; } if (options.filter !== undefined) { this.options.filter = options.filter; } if (options.showButton !== undefined) { this.options.showButton = options.showButton; } if (options.enabled !== undefined) { enabled = options.enabled; } } else if (typeof options === 'boolean') { this.options.filter = true; enabled = options; } else if (typeof options === 'function') { this.options.filter = options; enabled = true; } if (this.options.filter === false) { enabled = false; } this.options.enabled = enabled; } this._clean(); } }, { key: 'setModuleOptions', value: function setModuleOptions(moduleOptions) { this.moduleOptions = moduleOptions; if (this.options.enabled === true) { this._clean(); if (this.options.container !== undefined) { this.container = this.options.container; } this._create(); } } }, { key: '_create', /** * Create all DOM elements * @private */ value: function _create() { var _this = this; this._clean(); this.changedOptions = []; var filter = this.options.filter; var counter = 0; var show = false; for (var option in this.configureOptions) { if (this.configureOptions.hasOwnProperty(option)) { this.allowCreation = false; show = false; if (typeof filter === 'function') { show = filter(option, []); show = show || this._handleObject(this.configureOptions[option], [option], true); } else if (filter === true || filter.indexOf(option) !== -1) { show = true; } if (show !== false) { this.allowCreation = true; // linebreak between categories if (counter > 0) { this._makeItem([]); } // a header for the category this._makeHeader(option); // get the suboptions this._handleObject(this.configureOptions[option], [option]); } counter++; } } if (this.options.showButton === true) { (function () { var generateButton = document.createElement('div'); generateButton.className = 'vis-network-configuration button'; generateButton.innerHTML = 'generate options'; generateButton.onclick = function () { _this._printOptions(); }; generateButton.onmouseover = function () { generateButton.className = 'vis-network-configuration button hover'; }; generateButton.onmouseout = function () { generateButton.className = 'vis-network-configuration button'; }; _this.optionsContainer = document.createElement('div'); _this.optionsContainer.className = 'vis-network-configuration vis-option-container'; _this.domElements.push(_this.optionsContainer); _this.domElements.push(generateButton); })(); } this._push(); this.colorPicker.insertTo(this.container); } }, { key: '_push', /** * draw all DOM elements on the screen * @private */ value: function _push() { this.wrapper = document.createElement('div'); this.wrapper.className = 'vis-network-configuration-wrapper'; this.container.appendChild(this.wrapper); for (var i = 0; i < this.domElements.length; i++) { this.wrapper.appendChild(this.domElements[i]); } } }, { key: '_clean', /** * delete all DOM elements * @private */ value: function _clean() { for (var i = 0; i < this.domElements.length; i++) { this.wrapper.removeChild(this.domElements[i]); } if (this.wrapper !== undefined) { this.container.removeChild(this.wrapper); this.wrapper = undefined; } this.domElements = []; } }, { key: '_getValue', /** * get the value from the actualOptions if it exists * @param {array} path | where to look for the actual option * @returns {*} * @private */ value: function _getValue(path) { var base = this.moduleOptions; for (var i = 0; i < path.length; i++) { if (base[path[i]] !== undefined) { base = base[path[i]]; } else { base = undefined; break; } } return base; } }, { key: '_makeItem', /** * all option elements are wrapped in an item * @param path * @param domElements * @private */ value: function _makeItem(path) { var _this2 = this; for (var _len = arguments.length, domElements = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { domElements[_key - 1] = arguments[_key]; } if (this.allowCreation === true) { (function () { var item = document.createElement('div'); item.className = 'vis-network-configuration item s' + path.length; domElements.forEach(function (element) { item.appendChild(element); }); _this2.domElements.push(item); })(); } } }, { key: '_makeHeader', /** * header for major subjects * @param name * @private */ value: function _makeHeader(name) { var div = document.createElement('div'); div.className = 'vis-network-configuration header'; div.innerHTML = name; this._makeItem([], div); } }, { key: '_makeLabel', /** * make a label, if it is an object label, it gets different styling. * @param name * @param path * @param objectLabel * @returns {HTMLElement} * @private */ value: function _makeLabel(name, path) { var objectLabel = arguments[2] === undefined ? false : arguments[2]; var div = document.createElement('div'); div.className = 'vis-network-configuration label s' + path.length; if (objectLabel === true) { div.innerHTML = '' + name + ':'; } else { div.innerHTML = name + ':'; } return div; } }, { key: '_makeDropdown', /** * make a dropdown list for multiple possible string optoins * @param arr * @param value * @param path * @private */ value: function _makeDropdown(arr, value, path) { var select = document.createElement('select'); select.className = 'vis-network-configuration select'; var selectedValue = 0; if (value !== undefined) { if (arr.indexOf(value) !== -1) { selectedValue = arr.indexOf(value); } } for (var i = 0; i < arr.length; i++) { var option = document.createElement('option'); option.value = arr[i]; if (i === selectedValue) { option.selected = 'selected'; } option.innerHTML = arr[i]; select.appendChild(option); } var me = this; select.onchange = function () { me._update(this.value, path); }; var label = this._makeLabel(path[path.length - 1], path); this._makeItem(path, label, select); } }, { key: '_makeRange', /** * make a range object for numeric options * @param arr * @param value * @param path * @private */ value: function _makeRange(arr, value, path) { var defaultValue = arr[0]; var min = arr[1]; var max = arr[2]; var step = arr[3]; var range = document.createElement('input'); range.className = 'vis-network-configuration range'; try { range.type = 'range'; // not supported on IE9 range.min = min; range.max = max; } catch (err) {} range.step = step; if (value !== undefined) { if (value < 0 && value * 2 < min) { range.min = value * 2; } else if (value * 0.1 < min) { range.min = value / 10; } if (value * 2 > max && max !== 1) { range.max = value * 2; } range.value = value; } else { range.value = defaultValue; } var input = document.createElement('input'); input.className = 'vis-network-configuration rangeinput'; input.value = range.value; var me = this; range.onchange = function () { input.value = this.value;me._update(Number(this.value), path); }; range.oninput = function () { input.value = this.value; }; var label = this._makeLabel(path[path.length - 1], path); this._makeItem(path, label, range, input); } }, { key: '_makeCheckbox', /** * make a checkbox for boolean options. * @param defaultValue * @param value * @param path * @private */ value: function _makeCheckbox(defaultValue, value, path) { var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'vis-network-configuration checkbox'; checkbox.checked = defaultValue; if (value !== undefined) { checkbox.checked = value; if (value !== defaultValue) { if (typeof defaultValue === 'object') { if (value !== defaultValue.enabled) { this.changedOptions.push({ path: path, value: value }); } } else { this.changedOptions.push({ path: path, value: value }); } } } var me = this; checkbox.onchange = function () { me._update(this.checked, path); }; var label = this._makeLabel(path[path.length - 1], path); this._makeItem(path, label, checkbox); } }, { key: '_makeTextInput', /** * make a text input field for string options. * @param defaultValue * @param value * @param path * @private */ value: function _makeTextInput(defaultValue, value, path) { var checkbox = document.createElement('input'); checkbox.type = 'text'; checkbox.className = 'vis-network-configuration text'; checkbox.value = value; if (value !== defaultValue) { this.changedOptions.push({ path: path, value: value }); } var me = this; checkbox.onchange = function () { me._update(this.value, path); }; var label = this._makeLabel(path[path.length - 1], path); this._makeItem(path, label, checkbox); } }, { key: '_makeColorField', /** * make a color field with a color picker for color fields * @param arr * @param value * @param path * @private */ value: function _makeColorField(arr, value, path) { var _this3 = this; var defaultColor = arr[1]; var div = document.createElement('div'); value = value === undefined ? defaultColor : value; if (value !== 'none') { div.className = 'vis-network-configuration colorBlock'; div.style.backgroundColor = value; } else { div.className = 'vis-network-configuration colorBlock none'; } value = value === undefined ? defaultColor : value; div.onclick = function () { _this3._showColorPicker(value, div, path); }; var label = this._makeLabel(path[path.length - 1], path); this._makeItem(path, label, div); } }, { key: '_showColorPicker', /** * used by the color buttons to call the color picker. * @param event * @param value * @param div * @param path * @private */ value: function _showColorPicker(value, div, path) { var _this4 = this; var rect = div.getBoundingClientRect(); var bodyRect = document.body.getBoundingClientRect(); var pickerX = rect.left + rect.width + 5; var pickerY = rect.top - bodyRect.top + rect.height * 0.5; this.colorPicker.show(pickerX, pickerY); this.colorPicker.setColor(value); this.colorPicker.setCallback(function (color) { var colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')'; div.style.backgroundColor = colorString; _this4._update(colorString, path); }); } }, { key: '_handleObject', /** * parse an object and draw the correct items * @param obj * @param path * @private */ value: function _handleObject(obj) { var path = arguments[1] === undefined ? [] : arguments[1]; var checkOnly = arguments[2] === undefined ? false : arguments[2]; var show = false; var filter = this.options.filter; var visibleInSet = false; for (var subObj in obj) { if (obj.hasOwnProperty(subObj)) { show = true; var item = obj[subObj]; var newPath = util.copyAndExtendArray(path, subObj); if (typeof filter === 'function') { show = filter(subObj, path); // if needed we must go deeper into the object. if (show === false) { if (!(item instanceof Array) && typeof item !== 'string' && typeof item !== 'boolean' && item instanceof Object) { this.allowCreation = false; show = this._handleObject(item, newPath, true); this.allowCreation = checkOnly === false; } } } if (show !== false) { visibleInSet = true; var value = this._getValue(newPath); if (item instanceof Array) { this._handleArray(item, value, newPath); } else if (typeof item === 'string') { this._makeTextInput(item, value, newPath); } else if (typeof item === 'boolean') { this._makeCheckbox(item, value, newPath); } else if (item instanceof Object) { // collapse the physics options that are not enabled var draw = true; if (path.indexOf('physics') !== -1) { if (this.moduleOptions.physics.solver !== subObj) { draw = false; } } if (draw === true) { // initially collapse options with an disabled enabled option. if (item.enabled !== undefined) { var enabledPath = util.copyAndExtendArray(newPath, 'enabled'); var enabledValue = this._getValue(enabledPath); if (enabledValue === true) { var label = this._makeLabel(subObj, newPath, true); this._makeItem(newPath, label); visibleInSet = this._handleObject(item, newPath) || visibleInSet; } else { this._makeCheckbox(item, enabledValue, newPath); } } else { var label = this._makeLabel(subObj, newPath, true); this._makeItem(newPath, label); visibleInSet = this._handleObject(item, newPath) || visibleInSet; } } } else { console.error('dont know how to handle', item, subObj, newPath); } } } } return visibleInSet; } }, { key: '_handleArray', /** * handle the array type of option * @param optionName * @param arr * @param value * @param path * @private */ value: function _handleArray(arr, value, path) { if (typeof arr[0] === 'string' && arr[0] === 'color') { this._makeColorField(arr, value, path); if (arr[1] !== value) { this.changedOptions.push({ path: path, value: value }); } } else if (typeof arr[0] === 'string') { this._makeDropdown(arr, value, path); if (arr[0] !== value) { this.changedOptions.push({ path: path, value: value }); } } else if (typeof arr[0] === 'number') { this._makeRange(arr, value, path); if (arr[0] !== value) { this.changedOptions.push({ path: path, value: Number(value) }); } } } }, { key: '_update', /** * called to update the network with the new settings. * @param value * @param path * @private */ value: function _update(value, path) { var options = this._constructOptions(value, path); this.parent.setOptions(options); } }, { key: '_constructOptions', value: function _constructOptions(value, path) { var optionsObj = arguments[2] === undefined ? {} : arguments[2]; var pointer = optionsObj; // when dropdown boxes can be string or boolean, we typecast it into correct types value = value === 'true' ? true : value; value = value === 'false' ? false : value; for (var i = 0; i < path.length; i++) { if (path[i] !== 'global') { if (pointer[path[i]] === undefined) { pointer[path[i]] = {}; } if (i !== path.length - 1) { pointer = pointer[path[i]]; } else { pointer[path[i]] = value; } } } return optionsObj; } }, { key: '_printOptions', value: function _printOptions() { var options = {}; for (var i = 0; i < this.changedOptions.length; i++) { this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options); } this.optionsContainer.innerHTML = '
var options = ' + JSON.stringify(options, null, 2) + '
'; } }]); return Configurator; })(); exports['default'] = Configurator; module.exports = exports['default']; /***/ }, /* 46 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var Hammer = __webpack_require__(23); var hammerUtil = __webpack_require__(28); var util = __webpack_require__(1); var ColorPicker = (function () { function ColorPicker() { var pixelRatio = arguments[0] === undefined ? 1 : arguments[0]; _classCallCheck(this, ColorPicker); this.pixelRatio = pixelRatio; this.generated = false; this.centerCoordinates = { x: 289 / 2, y: 289 / 2 }; this.r = 289 * 0.49; this.color = { r: 255, g: 255, b: 255, a: 1 }; this.hueCircle = undefined; this.initialColor = { r: 255, g: 255, b: 255, a: 1 }; this.previousColor = undefined; this.applied = false; // bound by this.updateCallback = function () {}; // create all DOM elements this._create(); } _createClass(ColorPicker, [{ key: 'insertTo', /** * this inserts the colorPicker into a div from the DOM * @param container */ value: function insertTo(container) { if (this.hammer !== undefined) { this.hammer.destroy(); this.hammer = undefined; } this.container = container; this.container.appendChild(this.frame); this._bindHammer(); this._setSize(); } }, { key: 'setCallback', /** * the callback is executed on apply and save. Bind it to the application * @param callback */ value: function setCallback(callback) { if (typeof callback === 'function') { this.updateCallback = callback; } else { throw new Error('Function attempted to set as colorPicker callback is not a function.'); } } }, { key: '_isColorString', value: function _isColorString(color) { var htmlColors = { black: '#000000', navy: '#000080', darkblue: '#00008B', mediumblue: '#0000CD', blue: '#0000FF', darkgreen: '#006400', green: '#008000', teal: '#008080', darkcyan: '#008B8B', deepskyblue: '#00BFFF', darkturquoise: '#00CED1', mediumspringgreen: '#00FA9A', lime: '#00FF00', springgreen: '#00FF7F', aqua: '#00FFFF', cyan: '#00FFFF', midnightblue: '#191970', dodgerblue: '#1E90FF', lightseagreen: '#20B2AA', forestgreen: '#228B22', seagreen: '#2E8B57', darkslategray: '#2F4F4F', limegreen: '#32CD32', mediumseagreen: '#3CB371', turquoise: '#40E0D0', royalblue: '#4169E1', steelblue: '#4682B4', darkslateblue: '#483D8B', mediumturquoise: '#48D1CC', indigo: '#4B0082', darkolivegreen: '#556B2F', cadetblue: '#5F9EA0', cornflowerblue: '#6495ED', mediumaquamarine: '#66CDAA', dimgray: '#696969', slateblue: '#6A5ACD', olivedrab: '#6B8E23', slategray: '#708090', lightslategray: '#778899', mediumslateblue: '#7B68EE', lawngreen: '#7CFC00', chartreuse: '#7FFF00', aquamarine: '#7FFFD4', maroon: '#800000', purple: '#800080', olive: '#808000', gray: '#808080', skyblue: '#87CEEB', lightskyblue: '#87CEFA', blueviolet: '#8A2BE2', darkred: '#8B0000', darkmagenta: '#8B008B', saddlebrown: '#8B4513', darkseagreen: '#8FBC8F', lightgreen: '#90EE90', mediumpurple: '#9370D8', darkviolet: '#9400D3', palegreen: '#98FB98', darkorchid: '#9932CC', yellowgreen: '#9ACD32', sienna: '#A0522D', brown: '#A52A2A', darkgray: '#A9A9A9', lightblue: '#ADD8E6', greenyellow: '#ADFF2F', paleturquoise: '#AFEEEE', lightsteelblue: '#B0C4DE', powderblue: '#B0E0E6', firebrick: '#B22222', darkgoldenrod: '#B8860B', mediumorchid: '#BA55D3', rosybrown: '#BC8F8F', darkkhaki: '#BDB76B', silver: '#C0C0C0', mediumvioletred: '#C71585', indianred: '#CD5C5C', peru: '#CD853F', chocolate: '#D2691E', tan: '#D2B48C', lightgrey: '#D3D3D3', palevioletred: '#D87093', thistle: '#D8BFD8', orchid: '#DA70D6', goldenrod: '#DAA520', crimson: '#DC143C', gainsboro: '#DCDCDC', plum: '#DDA0DD', burlywood: '#DEB887', lightcyan: '#E0FFFF', lavender: '#E6E6FA', darksalmon: '#E9967A', violet: '#EE82EE', palegoldenrod: '#EEE8AA', lightcoral: '#F08080', khaki: '#F0E68C', aliceblue: '#F0F8FF', honeydew: '#F0FFF0', azure: '#F0FFFF', sandybrown: '#F4A460', wheat: '#F5DEB3', beige: '#F5F5DC', whitesmoke: '#F5F5F5', mintcream: '#F5FFFA', ghostwhite: '#F8F8FF', salmon: '#FA8072', antiquewhite: '#FAEBD7', linen: '#FAF0E6', lightgoldenrodyellow: '#FAFAD2', oldlace: '#FDF5E6', red: '#FF0000', fuchsia: '#FF00FF', magenta: '#FF00FF', deeppink: '#FF1493', orangered: '#FF4500', tomato: '#FF6347', hotpink: '#FF69B4', coral: '#FF7F50', darkorange: '#FF8C00', lightsalmon: '#FFA07A', orange: '#FFA500', lightpink: '#FFB6C1', pink: '#FFC0CB', gold: '#FFD700', peachpuff: '#FFDAB9', navajowhite: '#FFDEAD', moccasin: '#FFE4B5', bisque: '#FFE4C4', mistyrose: '#FFE4E1', blanchedalmond: '#FFEBCD', papayawhip: '#FFEFD5', lavenderblush: '#FFF0F5', seashell: '#FFF5EE', cornsilk: '#FFF8DC', lemonchiffon: '#FFFACD', floralwhite: '#FFFAF0', snow: '#FFFAFA', yellow: '#FFFF00', lightyellow: '#FFFFE0', ivory: '#FFFFF0', white: '#FFFFFF' }; if (typeof color === 'string') { return htmlColors[color]; } } }, { key: 'setColor', /** * Set the color of the colorPicker * Supported formats: * 'red' --> HTML color string * '#ffffff' --> hex string * 'rbg(255,255,255)' --> rgb string * 'rgba(255,255,255,1.0)' --> rgba string * {r:255,g:255,b:255} --> rgb object * {r:255,g:255,b:255,a:1.0} --> rgba object * @param color * @param setInitial */ value: function setColor(color) { var setInitial = arguments[1] === undefined ? true : arguments[1]; if (color === 'none') { return; } var rgba = undefined; // if a html color shorthand is used, convert to hex var htmlColor = this._isColorString(color); if (htmlColor !== undefined) { color = htmlColor; } // check format if (util.isString(color) === true) { if (util.isValidRGB(color) === true) { var rgbaArray = color.substr(4).substr(0, color.length - 5).split(','); rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: 1 }; } else if (util.isValidRGBA(color) === true) { var rgbaArray = color.substr(5).substr(0, color.length - 6).split(','); rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: rgbaArray[3] }; } else if (util.isValidHex(color) === true) { var rgbObj = util.hexToRGB(color); rgba = { r: rgbObj.r, g: rgbObj.g, b: rgbObj.b, a: 1 }; } } else { if (color instanceof Object) { if (color.r !== undefined && color.g !== undefined && color.b !== undefined) { var alpha = color.a !== undefined ? color.a : '1.0'; rgba = { r: color.r, g: color.g, b: color.b, a: alpha }; } } } // set color if (rgba === undefined) { throw new Error('Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: ' + JSON.stringify(color)); } else { this._setColor(rgba, setInitial); } } }, { key: 'show', /** * this shows the color picker at a location. The hue circle is constructed once and stored. * @param x * @param y */ value: function show(x, y) { this.applied = false; this.frame.style.display = 'block'; this.frame.style.top = y + 'px'; this.frame.style.left = x + 'px'; this._generateHueCircle(); } }, { key: '_hide', // ------------------------------------------ PRIVATE ----------------------------- // /** * Hide the picker. Is called by the cancel button. * Optional boolean to store the previous color for easy access later on. * @param storePrevious * @private */ value: function _hide() { var storePrevious = arguments[0] === undefined ? true : arguments[0]; // store the previous color for next time; if (storePrevious === true) { this.previousColor = util.extend({}, this.color); } if (this.applied === true) { this.updateCallback(this.initialColor); } this.frame.style.display = 'none'; } }, { key: '_save', /** * bound to the save button. Saves and hides. * @private */ value: function _save() { this.updateCallback(this.color); this.applied = false; this._hide(); } }, { key: '_apply', /** * Bound to apply button. Saves but does not close. Is undone by the cancel button. * @private */ value: function _apply() { this.applied = true; this.updateCallback(this.color); this._updatePicker(this.color); } }, { key: '_loadLast', /** * load the color from the previous session. * @private */ value: function _loadLast() { if (this.previousColor !== undefined) { this.setColor(this.previousColor, false); } else { alert('There is no last color to load...'); } } }, { key: '_setColor', /** * set the color, place the picker * @param rgba * @param setInitial * @private */ value: function _setColor(rgba) { var setInitial = arguments[1] === undefined ? true : arguments[1]; // store the initial color if (setInitial === true) { this.initialColor = util.extend({}, rgba); } this.color = rgba; var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); var angleConvert = 2 * Math.PI; var radius = this.r * hsv.s; var x = this.centerCoordinates.x + radius * Math.sin(angleConvert * hsv.h); var y = this.centerCoordinates.y + radius * Math.cos(angleConvert * hsv.h); this.colorPickerSelector.style.left = x - 0.5 * this.colorPickerSelector.clientWidth + 'px'; this.colorPickerSelector.style.top = y - 0.5 * this.colorPickerSelector.clientHeight + 'px'; this._updatePicker(rgba); } }, { key: '_setOpacity', /** * bound to opacity control * @param value * @private */ value: function _setOpacity(value) { this.color.a = value / 100; this._updatePicker(this.color); } }, { key: '_setBrightness', /** * bound to brightness control * @param value * @private */ value: function _setBrightness(value) { var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); hsv.v = value / 100; var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); rgba['a'] = this.color.a; this.color = rgba; this._updatePicker(); } }, { key: '_updatePicker', /** * update the colorpicker. A black circle overlays the hue circle to mimic the brightness decreasing. * @param rgba * @private */ value: function _updatePicker() { var rgba = arguments[0] === undefined ? this.color : arguments[0]; var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); var ctx = this.colorPickerCanvas.getContext('2d'); if (this.pixelRation === undefined) { this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); // clear the canvas var w = this.colorPickerCanvas.clientWidth; var h = this.colorPickerCanvas.clientHeight; ctx.clearRect(0, 0, w, h); ctx.putImageData(this.hueCircle, 0, 0); ctx.fillStyle = 'rgba(0,0,0,' + (1 - hsv.v) + ')'; ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); ctx.fill(); this.brightnessRange.value = 100 * hsv.v; this.opacityRange.value = 100 * rgba.a; this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; } }, { key: '_setSize', /** * used by create to set the size of the canvas. * @private */ value: function _setSize() { this.colorPickerCanvas.style.width = '100%'; this.colorPickerCanvas.style.height = '100%'; this.colorPickerCanvas.width = 289 * this.pixelRatio; this.colorPickerCanvas.height = 289 * this.pixelRatio; } }, { key: '_create', /** * create all dom elements * TODO: cleanup, lots of similar dom elements * @private */ value: function _create() { this.frame = document.createElement('div'); this.frame.className = 'vis-color-picker'; this.colorPickerDiv = document.createElement('div'); this.colorPickerSelector = document.createElement('div'); this.colorPickerSelector.className = 'vis-selector'; this.colorPickerDiv.appendChild(this.colorPickerSelector); this.colorPickerCanvas = document.createElement('canvas'); this.colorPickerDiv.appendChild(this.colorPickerCanvas); if (!this.colorPickerCanvas.getContext) { var noCanvas = document.createElement('DIV'); noCanvas.style.color = 'red'; noCanvas.style.fontWeight = 'bold'; noCanvas.style.padding = '10px'; noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; this.colorPickerCanvas.appendChild(noCanvas); } else { var ctx = this.colorPickerCanvas.getContext('2d'); this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); this.colorPickerCanvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } this.colorPickerDiv.className = 'vis-color'; this.opacityDiv = document.createElement('div'); this.opacityDiv.className = 'vis-opacity'; this.brightnessDiv = document.createElement('div'); this.brightnessDiv.className = 'vis-brightness'; this.arrowDiv = document.createElement('div'); this.arrowDiv.className = 'vis-arrow'; this.opacityRange = document.createElement('input'); try { this.opacityRange.type = 'range'; // Not supported on IE9 this.opacityRange.min = '0'; this.opacityRange.max = '100'; } catch (err) {} this.opacityRange.value = '100'; this.opacityRange.className = 'vis-range'; this.brightnessRange = document.createElement('input'); try { this.brightnessRange.type = 'range'; // Not supported on IE9 this.brightnessRange.min = '0'; this.brightnessRange.max = '100'; } catch (err) {} this.brightnessRange.value = '100'; this.brightnessRange.className = 'vis-range'; this.opacityDiv.appendChild(this.opacityRange); this.brightnessDiv.appendChild(this.brightnessRange); var me = this; this.opacityRange.onchange = function () { me._setOpacity(this.value); }; this.opacityRange.oninput = function () { me._setOpacity(this.value); }; this.brightnessRange.onchange = function () { me._setBrightness(this.value); }; this.brightnessRange.oninput = function () { me._setBrightness(this.value); }; this.brightnessLabel = document.createElement('div'); this.brightnessLabel.className = 'vis-label vis-brightness'; this.brightnessLabel.innerHTML = 'brightness:'; this.opacityLabel = document.createElement('div'); this.opacityLabel.className = 'vis-label vis-opacity'; this.opacityLabel.innerHTML = 'opacity:'; this.newColorDiv = document.createElement('div'); this.newColorDiv.className = 'vis-new-color'; this.newColorDiv.innerHTML = 'new'; this.initialColorDiv = document.createElement('div'); this.initialColorDiv.className = 'vis-initial-color'; this.initialColorDiv.innerHTML = 'initial'; this.cancelButton = document.createElement('div'); this.cancelButton.className = 'vis-button vis-cancel'; this.cancelButton.innerHTML = 'cancel'; this.cancelButton.onclick = this._hide.bind(this, false); this.applyButton = document.createElement('div'); this.applyButton.className = 'vis-button vis-apply'; this.applyButton.innerHTML = 'apply'; this.applyButton.onclick = this._apply.bind(this); this.saveButton = document.createElement('div'); this.saveButton.className = 'vis-button vis-save'; this.saveButton.innerHTML = 'save'; this.saveButton.onclick = this._save.bind(this); this.loadButton = document.createElement('div'); this.loadButton.className = 'vis-button vis-load'; this.loadButton.innerHTML = 'load last'; this.loadButton.onclick = this._loadLast.bind(this); this.frame.appendChild(this.colorPickerDiv); this.frame.appendChild(this.arrowDiv); this.frame.appendChild(this.brightnessLabel); this.frame.appendChild(this.brightnessDiv); this.frame.appendChild(this.opacityLabel); this.frame.appendChild(this.opacityDiv); this.frame.appendChild(this.newColorDiv); this.frame.appendChild(this.initialColorDiv); this.frame.appendChild(this.cancelButton); this.frame.appendChild(this.applyButton); this.frame.appendChild(this.saveButton); this.frame.appendChild(this.loadButton); } }, { key: '_bindHammer', /** * bind hammer to the color picker * @private */ value: function _bindHammer() { var _this = this; this.drag = {}; this.pinch = {}; this.hammer = new Hammer(this.colorPickerCanvas); this.hammer.get('pinch').set({ enable: true }); hammerUtil.onTouch(this.hammer, function (event) { _this._moveSelector(event); }); this.hammer.on('tap', function (event) { _this._moveSelector(event); }); this.hammer.on('panstart', function (event) { _this._moveSelector(event); }); this.hammer.on('panmove', function (event) { _this._moveSelector(event); }); this.hammer.on('panend', function (event) { _this._moveSelector(event); }); } }, { key: '_generateHueCircle', /** * generate the hue circle. This is relatively heavy (200ms) and is done only once on the first time it is shown. * @private */ value: function _generateHueCircle() { if (this.generated === false) { var ctx = this.colorPickerCanvas.getContext('2d'); if (this.pixelRation === undefined) { this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); // clear the canvas var w = this.colorPickerCanvas.clientWidth; var h = this.colorPickerCanvas.clientHeight; ctx.clearRect(0, 0, w, h); // draw hue circle var x = undefined, y = undefined, hue = undefined, sat = undefined; this.centerCoordinates = { x: w * 0.5, y: h * 0.5 }; this.r = 0.49 * w; var angleConvert = 2 * Math.PI / 360; var hfac = 1 / 360; var sfac = 1 / this.r; var rgb = undefined; for (hue = 0; hue < 360; hue++) { for (sat = 0; sat < this.r; sat++) { x = this.centerCoordinates.x + sat * Math.sin(angleConvert * hue); y = this.centerCoordinates.y + sat * Math.cos(angleConvert * hue); rgb = util.HSVToRGB(hue * hfac, sat * sfac, 1); ctx.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'; ctx.fillRect(x - 0.5, y - 0.5, 2, 2); } } ctx.strokeStyle = 'rgba(0,0,0,1)'; ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); ctx.stroke(); this.hueCircle = ctx.getImageData(0, 0, w, h); } this.generated = true; } }, { key: '_moveSelector', /** * move the selector. This is called by hammer functions. * * @param event * @private */ value: function _moveSelector(event) { var rect = this.colorPickerDiv.getBoundingClientRect(); var left = event.center.x - rect.left; var top = event.center.y - rect.top; var centerY = 0.5 * this.colorPickerDiv.clientHeight; var centerX = 0.5 * this.colorPickerDiv.clientWidth; var x = left - centerX; var y = top - centerY; var angle = Math.atan2(x, y); var radius = 0.98 * Math.min(Math.sqrt(x * x + y * y), centerX); var newTop = Math.cos(angle) * radius + centerY; var newLeft = Math.sin(angle) * radius + centerX; this.colorPickerSelector.style.top = newTop - 0.5 * this.colorPickerSelector.clientHeight + 'px'; this.colorPickerSelector.style.left = newLeft - 0.5 * this.colorPickerSelector.clientWidth + 'px'; // set color var h = angle / (2 * Math.PI); h = h < 0 ? h + 1 : h; var s = radius / this.r; var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); hsv.h = h; hsv.s = s; var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); rgba['a'] = this.color.a; this.color = rgba; // update previews this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; } }]); return ColorPicker; })(); exports['default'] = ColorPicker; module.exports = exports['default']; /***/ }, /* 47 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); var errorFound = false; var allOptions = undefined; var printStyle = 'background: #FFeeee; color: #dd0000'; /** * Used to validate options. */ var Validator = (function () { function Validator() { _classCallCheck(this, Validator); } _createClass(Validator, null, [{ key: 'validate', /** * Main function to be called * @param options * @param subObject * @returns {boolean} */ value: function validate(options, referenceOptions, subObject) { errorFound = false; allOptions = referenceOptions; var usedOptions = referenceOptions; if (subObject !== undefined) { usedOptions = referenceOptions[subObject]; } Validator.parse(options, usedOptions, []); return errorFound; } }, { key: 'parse', /** * Will traverse an object recursively and check every value * @param options * @param referenceOptions * @param path */ value: function parse(options, referenceOptions, path) { for (var option in options) { if (options.hasOwnProperty(option)) { Validator.check(option, options, referenceOptions, path); } } } }, { key: 'check', /** * Check every value. If the value is an object, call the parse function on that object. * @param option * @param options * @param referenceOptions * @param path */ value: function check(option, options, referenceOptions, path) { if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) { Validator.getSuggestion(option, referenceOptions, path); } else if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) { // __any__ is a wildcard. Any value is accepted and will be further analysed by reference. if (Validator.getType(options[option]) === 'object' && referenceOptions['__any__'].__type__ !== undefined) { // if the any subgroup is not a predefined object int he configurator we do not look deeper into the object. Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'].__type__, path); } else { Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'], path); } } else { // Since all options in the reference are objects, we can check whether they are supposed to be object to look for the __type__ field. if (referenceOptions[option].__type__ !== undefined) { // if this should be an object, we check if the correct type has been supplied to account for shorthand options. Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option].__type__, path); } else { Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option], path); } } } }, { key: 'checkFields', /** * * @param {String} option | the option property * @param {Object} options | The supplied options object * @param {Object} referenceOptions | The reference options containing all options and their allowed formats * @param {String} referenceOption | Usually this is the same as option, except when handling an __any__ tag. * @param {String} refOptionType | This is the type object from the reference options * @param {Array} path | where in the object is the option */ value: function checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) { var optionType = Validator.getType(options[option]); var refOptionType = refOptionObj[optionType]; if (refOptionType !== undefined) { // if the type is correct, we check if it is supposed to be one of a few select values if (Validator.getType(refOptionType) === 'array') { if (refOptionType.indexOf(options[option]) === -1) { console.log('%cInvalid option detected in "' + option + '".' + ' Allowed values are:' + Validator.print(refOptionType) + ' not "' + options[option] + '". ' + Validator.printLocation(path, option), printStyle); errorFound = true; } else if (optionType === 'object' && referenceOption !== '__any__') { path = util.copyAndExtendArray(path, option); Validator.parse(options[option], referenceOptions[referenceOption], path); } } else if (optionType === 'object' && referenceOption !== '__any__') { path = util.copyAndExtendArray(path, option); Validator.parse(options[option], referenceOptions[referenceOption], path); } } else if (refOptionObj['any'] === undefined) { // type of the field is incorrect and the field cannot be any console.log('%cInvalid type received for "' + option + '". Expected: ' + Validator.print(Object.keys(refOptionObj)) + '. Received [' + optionType + '] "' + options[option] + '"' + Validator.printLocation(path, option), printStyle); errorFound = true; } } }, { key: 'getType', value: function getType(object) { var type = typeof object; if (type === 'object') { if (object === null) { return 'null'; } if (object instanceof Boolean) { return 'boolean'; } if (object instanceof Number) { return 'number'; } if (object instanceof String) { return 'string'; } if (Array.isArray(object)) { return 'array'; } if (object instanceof Date) { return 'date'; } if (object.nodeType !== undefined) { return 'dom'; } if (object._isAMomentObject === true) { return 'moment'; } return 'object'; } else if (type === 'number') { return 'number'; } else if (type === 'boolean') { return 'boolean'; } else if (type === 'string') { return 'string'; } else if (type === undefined) { return 'undefined'; } return type; } }, { key: 'getSuggestion', value: function getSuggestion(option, options, path) { var localSearch = Validator.findInOptions(option, options, path, false); var globalSearch = Validator.findInOptions(option, allOptions, [], true); var localSearchThreshold = 8; var globalSearchThreshold = 4; if (localSearch.indexMatch !== undefined) { console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was incomplete? Did you mean: "' + localSearch.indexMatch + '"?\n\n', printStyle); } else if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) { console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was misplaced? Matching option found at: ' + Validator.printLocation(globalSearch.path, globalSearch.closestMatch, ''), printStyle); } else if (localSearch.distance <= localSearchThreshold) { console.log('%cUnknown option detected: "' + option + '". Did you mean "' + localSearch.closestMatch + '"?' + Validator.printLocation(localSearch.path, option), printStyle); } else { console.log('%cUnknown option detected: "' + option + '". Did you mean one of these: ' + Validator.print(Object.keys(options)) + Validator.printLocation(path, option), printStyle); } errorFound = true; } }, { key: 'findInOptions', /** * traverse the options in search for a match. * @param option * @param options * @param path * @param recursive * @returns {{closestMatch: string, path: Array, distance: number}} */ value: function findInOptions(option, options, path) { var recursive = arguments[3] === undefined ? false : arguments[3]; var min = 1000000000; var closestMatch = ''; var closestMatchPath = []; var lowerCaseOption = option.toLowerCase(); var indexMatch = undefined; for (var op in options) { var distance = undefined; if (options[op].__type__ !== undefined && recursive === true) { var result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path, op)); if (min > result.distance) { closestMatch = result.closestMatch; closestMatchPath = result.path; min = result.distance; indexMatch = result.indexMatch; } } else { if (op.toLowerCase().indexOf(lowerCaseOption) !== -1) { indexMatch = op; } distance = Validator.levenshteinDistance(option, op); if (min > distance) { closestMatch = op; closestMatchPath = util.copyArray(path); min = distance; } } } return { closestMatch: closestMatch, path: closestMatchPath, distance: min, indexMatch: indexMatch }; } }, { key: 'printLocation', value: function printLocation(path, option) { var prefix = arguments[2] === undefined ? 'Problem value found at: \n' : arguments[2]; var str = '\n\n' + prefix + 'options = {\n'; for (var i = 0; i < path.length; i++) { for (var j = 0; j < i + 1; j++) { str += ' '; } str += path[i] + ': {\n'; } for (var j = 0; j < path.length + 1; j++) { str += ' '; } str += option + '\n'; for (var i = 0; i < path.length + 1; i++) { for (var j = 0; j < path.length - i; j++) { str += ' '; } str += '}\n'; } return str + '\n\n'; } }, { key: 'print', value: function print(options) { return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, '').replace(/(\,)/g, ', '); } }, { key: 'levenshteinDistance', // Compute the edit distance between the two given strings // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript /* Copyright (c) 2011 Andrei Mackenzie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ value: function levenshteinDistance(a, b) { if (a.length === 0) return b.length; if (b.length === 0) return a.length; var matrix = []; // increment along the first column of each row var i; for (i = 0; i <= b.length; i++) { matrix[i] = [i]; } // increment each column in the first row var j; for (j = 0; j <= a.length; j++) { matrix[0][j] = j; } // Fill in the rest of the matrix for (i = 1; i <= b.length; i++) { for (j = 1; j <= a.length; j++) { if (b.charAt(i - 1) == a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution Math.min(matrix[i][j - 1] + 1, // insertion matrix[i - 1][j] + 1)); // deletion } } } return matrix[b.length][a.length]; } }]); return Validator; })(); exports['default'] = Validator; exports.printStyle = printStyle; /***/ }, /* 48 */ /***/ function(module, exports) { /** * This object contains all possible options. It will check if the types are correct, if required if the option is one * of the allowed values. * * __any__ means that the name of the property does not matter. * __type__ is a required field for all objects and contains the allowed types of all objects */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var string = 'string'; var boolean = 'boolean'; var number = 'number'; var array = 'array'; var date = 'date'; var object = 'object'; // should only be in a __type__ property var dom = 'dom'; var moment = 'moment'; var any = 'any'; var allOptions = { configure: { enabled: { boolean: boolean }, filter: { boolean: boolean, 'function': 'function' }, container: { dom: dom }, __type__: { object: object, boolean: boolean, 'function': 'function' } }, //globals : align: { string: string }, autoResize: { boolean: boolean }, clickToUse: { boolean: boolean }, dataAttributes: { string: string, array: array }, editable: { add: { boolean: boolean, 'undefined': 'undefined' }, remove: { boolean: boolean, 'undefined': 'undefined' }, updateGroup: { boolean: boolean, 'undefined': 'undefined' }, updateTime: { boolean: boolean, 'undefined': 'undefined' }, __type__: { boolean: boolean, object: object } }, end: { number: number, date: date, string: string, moment: moment }, format: { minorLabels: { millisecond: { string: string, 'undefined': 'undefined' }, second: { string: string, 'undefined': 'undefined' }, minute: { string: string, 'undefined': 'undefined' }, hour: { string: string, 'undefined': 'undefined' }, weekday: { string: string, 'undefined': 'undefined' }, day: { string: string, 'undefined': 'undefined' }, month: { string: string, 'undefined': 'undefined' }, year: { string: string, 'undefined': 'undefined' }, __type__: { object: object } }, majorLabels: { millisecond: { string: string, 'undefined': 'undefined' }, second: { string: string, 'undefined': 'undefined' }, minute: { string: string, 'undefined': 'undefined' }, hour: { string: string, 'undefined': 'undefined' }, weekday: { string: string, 'undefined': 'undefined' }, day: { string: string, 'undefined': 'undefined' }, month: { string: string, 'undefined': 'undefined' }, year: { string: string, 'undefined': 'undefined' }, __type__: { object: object } }, __type__: { object: object } }, groupOrder: { string: string, 'function': 'function' }, height: { string: string, number: number }, hiddenDates: { object: object, array: array }, locale: { string: string }, locales: { __any__: { any: any }, __type__: { object: object } }, margin: { axis: { number: number }, item: { horizontal: { number: number, 'undefined': 'undefined' }, vertical: { number: number, 'undefined': 'undefined' }, __type__: { object: object, number: number } }, __type__: { object: object, number: number } }, max: { date: date, number: number, string: string, moment: moment }, maxHeight: { number: number, string: string }, min: { date: date, number: number, string: string, moment: moment }, minHeight: { number: number, string: string }, moveable: { boolean: boolean }, multiselect: { boolean: boolean }, onAdd: { 'function': 'function' }, onUpdate: { 'function': 'function' }, onMove: { 'function': 'function' }, onMoving: { 'function': 'function' }, onRemove: { 'function': 'function' }, order: { 'function': 'function' }, orientation: { axis: { string: string, 'undefined': 'undefined' }, item: { string: string, 'undefined': 'undefined' }, __type__: { string: string, object: object } }, selectable: { boolean: boolean }, showCurrentTime: { boolean: boolean }, showMajorLabels: { boolean: boolean }, showMinorLabels: { boolean: boolean }, stack: { boolean: boolean }, snap: { 'function': 'function', 'null': 'null' }, start: { date: date, number: number, string: string, moment: moment }, template: { 'function': 'function' }, timeAxis: { scale: { string: string, 'undefined': 'undefined' }, step: { number: number, 'undefined': 'undefined' }, __type__: { object: object } }, type: { string: string }, width: { string: string, number: number }, zoomable: { boolean: boolean }, zoomMax: { number: number }, zoomMin: { number: number }, __type__: { object: object } }; var configureOptions = { global: { align: ['center', 'left', 'right'], autoResize: true, clickToUse: false, // dataAttributes: ['all'], // FIXME: can be 'all' or string[] editable: { add: false, remove: false, updateGroup: false, updateTime: false }, end: '', format: { minorLabels: { millisecond: 'SSS', second: 's', minute: 'HH:mm', hour: 'HH:mm', weekday: 'ddd D', day: 'D', month: 'MMM', year: 'YYYY' }, majorLabels: { millisecond: 'HH:mm:ss', second: 'D MMMM HH:mm', minute: 'ddd D MMMM', hour: 'ddd D MMMM', weekday: 'MMMM YYYY', day: 'MMMM YYYY', month: 'YYYY', year: '' } }, //groupOrder: {string, 'function': 'function'}, height: '', //hiddenDates: {object, array}, locale: '', margin: { axis: [20, 0, 100, 1], item: { horizontal: [10, 0, 100, 1], vertical: [10, 0, 100, 1] } }, max: '', maxHeight: '', min: '', minHeight: '', moveable: false, multiselect: false, //onAdd: {'function': 'function'}, //onUpdate: {'function': 'function'}, //onMove: {'function': 'function'}, //onMoving: {'function': 'function'}, //onRename: {'function': 'function'}, //order: {'function': 'function'}, orientation: { axis: ['both', 'bottom', 'top'], item: ['bottom', 'top'] }, selectable: true, showCurrentTime: false, showMajorLabels: true, showMinorLabels: true, stack: true, //snap: {'function': 'function', nada}, start: '', //template: {'function': 'function'}, //timeAxis: { // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'], // step: [1, 1, 10, 1] //}, type: ['box', 'point', 'range', 'background'], width: '100%', zoomable: true, zoomMax: [315360000000000, 10, 315360000000000, 1], zoomMin: [10, 10, 315360000000000, 1] } }; exports.allOptions = allOptions; exports.configureOptions = configureOptions; /***/ }, /* 49 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var Emitter = __webpack_require__(13); var Hammer = __webpack_require__(23); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var Range = __webpack_require__(27); var Core = __webpack_require__(30); var TimeAxis = __webpack_require__(41); var CurrentTime = __webpack_require__(20); var CustomTime = __webpack_require__(44); var LineGraph = __webpack_require__(50); var Configurator = __webpack_require__(45); var Validator = __webpack_require__(47)['default']; var printStyle = __webpack_require__(47).printStyle; var allOptions = __webpack_require__(58).allOptions; var configureOptions = __webpack_require__(58).configureOptions; /** * Create a timeline visualization * @param {HTMLElement} container * @param {vis.DataSet | Array} [items] * @param {Object} [options] See Graph2d.setOptions for the available options. * @constructor * @extends Core */ function Graph2d(container, items, groups, options) { // if the third element is options, the forth is groups (optionally); if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) { var forthArgument = options; options = groups; groups = forthArgument; } var me = this; this.defaultOptions = { start: null, end: null, autoResize: true, orientation: { axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' item: 'bottom' // not relevant for Graph2d }, width: null, height: null, maxHeight: null, minHeight: null }; this.options = util.deepExtend({}, this.defaultOptions); // Create the DOM, props, and emitter this._create(container); // all components listed here will be repainted automatically this.components = []; this.body = { dom: this.dom, domProps: this.props, emitter: { on: this.on.bind(this), off: this.off.bind(this), emit: this.emit.bind(this) }, hiddenDates: [], util: { toScreen: me._toScreen.bind(me), toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toTime: me._toTime.bind(me), toGlobalTime: me._toGlobalTime.bind(me) } }; // range this.range = new Range(this.body); this.components.push(this.range); this.body.range = this.range; // time axis this.timeAxis = new TimeAxis(this.body); this.components.push(this.timeAxis); //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); // current time bar this.currentTime = new CurrentTime(this.body); this.components.push(this.currentTime); // item set this.linegraph = new LineGraph(this.body); this.components.push(this.linegraph); this.itemsData = null; // DataSet this.groupsData = null; // DataSet this.on('tap', function (event) { me.emit('click', me.getEventProperties(event)); }); this.on('doubletap', function (event) { me.emit('doubleClick', me.getEventProperties(event)); }); this.dom.root.oncontextmenu = function (event) { me.emit('contextmenu', me.getEventProperties(event)); }; // apply options if (options) { this.setOptions(options); } // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! if (groups) { this.setGroups(groups); } // create itemset if (items) { this.setItems(items); } else { this._redraw(); } } // Extend the functionality from Core Graph2d.prototype = new Core(); Graph2d.prototype.setOptions = function (options) { // validate options var errorFound = Validator.validate(options, allOptions); if (errorFound === true) { console.log('%cErrors have been found in the supplied options object.', printStyle); } Core.prototype.setOptions.call(this, options); }; /** * Set items * @param {vis.DataSet | Array | null} items */ Graph2d.prototype.setItems = function (items) { var initialLoad = this.itemsData == null; // convert to type DataSet when needed var newDataSet; if (!items) { newDataSet = null; } else if (items instanceof DataSet || items instanceof DataView) { newDataSet = items; } else { // turn an array into a dataset newDataSet = new DataSet(items, { type: { start: 'Date', end: 'Date' } }); } // set items this.itemsData = newDataSet; this.linegraph && this.linegraph.setItems(newDataSet); if (initialLoad) { if (this.options.start != undefined || this.options.end != undefined) { var start = this.options.start != undefined ? this.options.start : null; var end = this.options.end != undefined ? this.options.end : null; this.setWindow(start, end, { animation: false }); } else { this.fit({ animation: false }); } } }; /** * Set groups * @param {vis.DataSet | Array} groups */ Graph2d.prototype.setGroups = function (groups) { // convert to type DataSet when needed var newDataSet; if (!groups) { newDataSet = null; } else if (groups instanceof DataSet || groups instanceof DataView) { newDataSet = groups; } else { // turn an array into a dataset newDataSet = new DataSet(groups); } this.groupsData = newDataSet; this.linegraph.setGroups(newDataSet); }; /** * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right). * @param groupId * @param width * @param height */ Graph2d.prototype.getLegend = function (groupId, width, height) { if (width === undefined) { width = 15; } if (height === undefined) { height = 15; } if (this.linegraph.groups[groupId] !== undefined) { return this.linegraph.groups[groupId].getLegend(width, height); } else { return 'cannot find group:' + groupId; } }; /** * This checks if the visible option of the supplied group (by ID) is true or false. * @param groupId * @returns {*} */ Graph2d.prototype.isGroupVisible = function (groupId) { if (this.linegraph.groups[groupId] !== undefined) { return this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true); } else { return false; } }; /** * Get the data range of the item set. * @returns {{min: Date, max: Date}} range A range with a start and end Date. * When no minimum is found, min==null * When no maximum is found, max==null */ Graph2d.prototype.getDataRange = function () { var min = null; var max = null; // calculate min from start filed for (var groupId in this.linegraph.groups) { if (this.linegraph.groups.hasOwnProperty(groupId)) { if (this.linegraph.groups[groupId].visible == true) { for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { var item = this.linegraph.groups[groupId].itemsData[i]; var value = util.convert(item.x, 'Date').valueOf(); min = min == null ? value : min > value ? value : min; max = max == null ? value : max < value ? value : max; } } } } return { min: min != null ? new Date(min) : null, max: max != null ? new Date(max) : null }; }; /** * Generate Timeline related information from an event * @param {Event} event * @return {Object} An object with related information, like on which area * The event happened, whether clicked on an item, etc. */ Graph2d.prototype.getEventProperties = function (event) { var clientX = event.center ? event.center.x : event.clientX; var clientY = event.center ? event.center.y : event.clientY; var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); var time = this._toTime(x); var customTime = CustomTime.customTimeFromTarget(event); var element = util.getTarget(event); var what = null; if (util.hasParent(element, this.timeAxis.dom.foreground)) { what = 'axis'; } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { what = 'axis'; } else if (util.hasParent(element, this.linegraph.yAxisLeft.dom.frame)) { what = 'data-axis'; } else if (util.hasParent(element, this.linegraph.yAxisRight.dom.frame)) { what = 'data-axis'; } else if (util.hasParent(element, this.linegraph.legendLeft.dom.frame)) { what = 'legend'; } else if (util.hasParent(element, this.linegraph.legendRight.dom.frame)) { what = 'legend'; } else if (customTime != null) { what = 'custom-time'; } else if (util.hasParent(element, this.currentTime.bar)) { what = 'current-time'; } else if (util.hasParent(element, this.dom.center)) { what = 'background'; } var value = []; var yAxisLeft = this.linegraph.yAxisLeft; var yAxisRight = this.linegraph.yAxisRight; if (!yAxisLeft.hidden) { value.push(yAxisLeft.screenToValue(y)); } if (!yAxisRight.hidden) { value.push(yAxisRight.screenToValue(y)); } return { event: event, what: what, pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, x: x, y: y, time: time, value: value }; }; /** * Load a configurator * @return {Object} * @private */ Graph2d.prototype._createConfigurator = function () { return new Configurator(this, this.dom.container, configureOptions); }; module.exports = Graph2d; /***/ }, /* 50 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var DOMutil = __webpack_require__(7); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var Component = __webpack_require__(21); var DataAxis = __webpack_require__(51); var GraphGroup = __webpack_require__(53); var Legend = __webpack_require__(57); var BarFunctions = __webpack_require__(56); var LineFunctions = __webpack_require__(54); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items /** * This is the constructor of the LineGraph. It requires a Timeline body and options. * * @param body * @param options * @constructor */ function LineGraph(body, options) { this.id = util.randomUUID(); this.body = body; this.defaultOptions = { yAxisOrientation: 'left', defaultGroup: 'default', sort: true, sampling: true, stack: false, graphHeight: '400px', shaded: { enabled: false, orientation: 'bottom' // top, bottom }, style: 'line', // line, bar barChart: { width: 50, sideBySide: false, align: 'center' // left, center, right }, interpolation: { enabled: true, parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) alpha: 0.5 }, drawPoints: { enabled: true, size: 6, style: 'square' // square, circle }, dataAxis: { showMinorLabels: true, showMajorLabels: true, icons: false, width: '40px', visible: true, alignZeros: true, left: { range: { min: undefined, max: undefined }, format: function format(value) { return value; }, title: { text: undefined, style: undefined } }, right: { range: { min: undefined, max: undefined }, format: function format(value) { return value; }, title: { text: undefined, style: undefined } } }, legend: { enabled: false, icons: true, left: { visible: true, position: 'top-left' // top/bottom - left,right }, right: { visible: true, position: 'top-right' // top/bottom - left,right } }, groups: { visibility: {} } }; // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); this.dom = {}; this.props = {}; this.hammer = null; this.groups = {}; this.abortedGraphUpdate = false; this.updateSVGheight = false; this.updateSVGheightOnResize = false; var me = this; this.itemsData = null; // DataSet this.groupsData = null; // DataSet // listeners for the DataSet of the items this.itemListeners = { 'add': function add(event, params, senderId) { me._onAdd(params.items); }, 'update': function update(event, params, senderId) { me._onUpdate(params.items); }, 'remove': function remove(event, params, senderId) { me._onRemove(params.items); } }; // listeners for the DataSet of the groups this.groupListeners = { 'add': function add(event, params, senderId) { me._onAddGroups(params.items); }, 'update': function update(event, params, senderId) { me._onUpdateGroups(params.items); }, 'remove': function remove(event, params, senderId) { me._onRemoveGroups(params.items); } }; this.items = {}; // object with an Item for every data item this.selection = []; // list with the ids of all selected nodes this.lastStart = this.body.range.start; this.touchParams = {}; // stores properties while dragging this.svgElements = {}; this.setOptions(options); this.groupsUsingDefaultStyles = [0]; this.COUNTER = 0; this.body.emitter.on('rangechanged', function () { me.lastStart = me.body.range.start; me.svg.style.left = util.option.asSize(-me.props.width); me.redraw.call(me, true); }); // create the HTML DOM this._create(); this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; this.body.emitter.emit('change'); } LineGraph.prototype = new Component(); /** * Create the HTML DOM for the ItemSet */ LineGraph.prototype._create = function () { var frame = document.createElement('div'); frame.className = 'vis-line-graph'; this.dom.frame = frame; // create svg element for graph drawing. this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svg.style.position = 'relative'; this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px'; this.svg.style.display = 'block'; frame.appendChild(this.svg); // data axis this.options.dataAxis.orientation = 'left'; this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); this.options.dataAxis.orientation = 'right'; this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); delete this.options.dataAxis.orientation; // legends this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups); this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups); this.show(); }; /** * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. * @param {object} options */ LineGraph.prototype.setOptions = function (options) { if (options) { var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups']; if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { this.updateSVGheight = true; this.updateSVGheightOnResize = true; } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { if (parseInt((options.graphHeight + '').replace('px', '')) < this.body.domProps.centerContainer.height) { this.updateSVGheight = true; } } util.selectiveDeepExtend(fields, this.options, options); util.mergeOptions(this.options, options, 'interpolation'); util.mergeOptions(this.options, options, 'drawPoints'); util.mergeOptions(this.options, options, 'shaded'); util.mergeOptions(this.options, options, 'legend'); if (options.interpolation) { if (typeof options.interpolation == 'object') { if (options.interpolation.parametrization) { if (options.interpolation.parametrization == 'uniform') { this.options.interpolation.alpha = 0; } else if (options.interpolation.parametrization == 'chordal') { this.options.interpolation.alpha = 1; } else { this.options.interpolation.parametrization = 'centripetal'; this.options.interpolation.alpha = 0.5; } } } } if (this.yAxisLeft) { if (options.dataAxis !== undefined) { this.yAxisLeft.setOptions(this.options.dataAxis); this.yAxisRight.setOptions(this.options.dataAxis); } } if (this.legendLeft) { if (options.legend !== undefined) { this.legendLeft.setOptions(this.options.legend); this.legendRight.setOptions(this.options.legend); } } if (this.groups.hasOwnProperty(UNGROUPED)) { this.groups[UNGROUPED].setOptions(options); } } // this is used to redraw the graph if the visibility of the groups is changed. if (this.dom.frame) { this.redraw(true); } }; /** * Hide the component from the DOM */ LineGraph.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); } }; /** * Show the component in the DOM (when not already visible). * @return {Boolean} changed */ LineGraph.prototype.show = function () { // show frame containing the items if (!this.dom.frame.parentNode) { this.body.dom.center.appendChild(this.dom.frame); } }; /** * Set items * @param {vis.DataSet | null} items */ LineGraph.prototype.setItems = function (items) { var me = this, ids, oldItemsData = this.itemsData; // replace the dataset if (!items) { this.itemsData = null; } else if (items instanceof DataSet || items instanceof DataView) { this.itemsData = items; } else { throw new TypeError('Data must be an instance of DataSet or DataView'); } if (oldItemsData) { // unsubscribe from old dataset util.forEach(this.itemListeners, function (callback, event) { oldItemsData.off(event, callback); }); // remove all drawn items ids = oldItemsData.getIds(); this._onRemove(ids); } if (this.itemsData) { // subscribe to new dataset var id = this.id; util.forEach(this.itemListeners, function (callback, event) { me.itemsData.on(event, callback, id); }); // add all new items ids = this.itemsData.getIds(); this._onAdd(ids); } this._updateUngrouped(); //this._updateGraph(); this.redraw(true); }; /** * Set groups * @param {vis.DataSet} groups */ LineGraph.prototype.setGroups = function (groups) { var me = this; var ids; // unsubscribe from current dataset if (this.groupsData) { util.forEach(this.groupListeners, function (callback, event) { me.groupsData.off(event, callback); }); // remove all drawn groups ids = this.groupsData.getIds(); this.groupsData = null; this._onRemoveGroups(ids); // note: this will cause a redraw } // replace the dataset if (!groups) { this.groupsData = null; } else if (groups instanceof DataSet || groups instanceof DataView) { this.groupsData = groups; } else { throw new TypeError('Data must be an instance of DataSet or DataView'); } if (this.groupsData) { // subscribe to new dataset var id = this.id; util.forEach(this.groupListeners, function (callback, event) { me.groupsData.on(event, callback, id); }); // draw all ms ids = this.groupsData.getIds(); this._onAddGroups(ids); } this._onUpdate(); }; /** * Update the data * @param [ids] * @private */ LineGraph.prototype._onUpdate = function (ids) { this._updateUngrouped(); this._updateAllGroupData(); //this._updateGraph(); this.redraw(true); }; LineGraph.prototype._onAdd = function (ids) { this._onUpdate(ids); }; LineGraph.prototype._onRemove = function (ids) { this._onUpdate(ids); }; LineGraph.prototype._onUpdateGroups = function (groupIds) { for (var i = 0; i < groupIds.length; i++) { var group = this.groupsData.get(groupIds[i]); this._updateGroup(group, groupIds[i]); } //this._updateGraph(); this.redraw(true); }; LineGraph.prototype._onAddGroups = function (groupIds) { this._onUpdateGroups(groupIds); }; /** * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph * @param {Array} groupIds * @private */ LineGraph.prototype._onRemoveGroups = function (groupIds) { for (var i = 0; i < groupIds.length; i++) { if (this.groups.hasOwnProperty(groupIds[i])) { if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') { this.yAxisRight.removeGroup(groupIds[i]); this.legendRight.removeGroup(groupIds[i]); this.legendRight.redraw(); } else { this.yAxisLeft.removeGroup(groupIds[i]); this.legendLeft.removeGroup(groupIds[i]); this.legendLeft.redraw(); } delete this.groups[groupIds[i]]; } } this._updateUngrouped(); //this._updateGraph(); this.redraw(true); }; /** * update a group object with the group dataset entree * * @param group * @param groupId * @private */ LineGraph.prototype._updateGroup = function (group, groupId) { if (!this.groups.hasOwnProperty(groupId)) { this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); if (this.groups[groupId].options.yAxisOrientation == 'right') { this.yAxisRight.addGroup(groupId, this.groups[groupId]); this.legendRight.addGroup(groupId, this.groups[groupId]); } else { this.yAxisLeft.addGroup(groupId, this.groups[groupId]); this.legendLeft.addGroup(groupId, this.groups[groupId]); } } else { this.groups[groupId].update(group); if (this.groups[groupId].options.yAxisOrientation == 'right') { this.yAxisRight.updateGroup(groupId, this.groups[groupId]); this.legendRight.updateGroup(groupId, this.groups[groupId]); } else { this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); this.legendLeft.updateGroup(groupId, this.groups[groupId]); } } this.legendLeft.redraw(); this.legendRight.redraw(); }; /** * this updates all groups, it is used when there is an update the the itemset. * * @private */ LineGraph.prototype._updateAllGroupData = function () { if (this.itemsData != null) { var groupsContent = {}; var groupId; for (groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { groupsContent[groupId] = []; } } for (var itemId in this.itemsData._data) { if (this.itemsData._data.hasOwnProperty(itemId)) { var item = this.itemsData._data[itemId]; if (groupsContent[item.group] === undefined) { throw new Error('Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups.'); } item.x = util.convert(item.x, 'Date'); groupsContent[item.group].push(item); } } for (groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { this.groups[groupId].setItems(groupsContent[groupId]); } } } }; /** * Create or delete the group holding all ungrouped items. This group is used when * there are no groups specified. This anonymous group is called 'graph'. * @protected */ LineGraph.prototype._updateUngrouped = function () { if (this.itemsData && this.itemsData != null) { var ungroupedCounter = 0; for (var itemId in this.itemsData._data) { if (this.itemsData._data.hasOwnProperty(itemId)) { var item = this.itemsData._data[itemId]; if (item != undefined) { if (item.hasOwnProperty('group')) { if (item.group === undefined) { item.group = UNGROUPED; } } else { item.group = UNGROUPED; } ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter; } } } if (ungroupedCounter == 0) { delete this.groups[UNGROUPED]; this.legendLeft.removeGroup(UNGROUPED); this.legendRight.removeGroup(UNGROUPED); this.yAxisLeft.removeGroup(UNGROUPED); this.yAxisRight.removeGroup(UNGROUPED); } else { var group = { id: UNGROUPED, content: this.options.defaultGroup }; this._updateGroup(group, UNGROUPED); } } else { delete this.groups[UNGROUPED]; this.legendLeft.removeGroup(UNGROUPED); this.legendRight.removeGroup(UNGROUPED); this.yAxisLeft.removeGroup(UNGROUPED); this.yAxisRight.removeGroup(UNGROUPED); } this.legendLeft.redraw(); this.legendRight.redraw(); }; /** * Redraw the component, mandatory function * @return {boolean} Returns true if the component is resized */ LineGraph.prototype.redraw = function (forceGraphUpdate) { var resized = false; // calculate actual size and position this.props.width = this.dom.frame.offsetWidth; this.props.height = this.body.domProps.centerContainer.height - this.body.domProps.border.top - this.body.domProps.border.bottom; // update the graph if there is no lastWidth or with, used for the initial draw if (this.lastWidth === undefined && this.props.width) { forceGraphUpdate = true; } // check if this component is resized resized = this._isResized() || resized; // check whether zoomed (in that case we need to re-stack everything) var visibleInterval = this.body.range.end - this.body.range.start; var zoomed = visibleInterval != this.lastVisibleInterval; this.lastVisibleInterval = visibleInterval; // the svg element is three times as big as the width, this allows for fully dragging left and right // without reloading the graph. the controls for this are bound to events in the constructor if (resized == true) { this.svg.style.width = util.option.asSize(3 * this.props.width); this.svg.style.left = util.option.asSize(-this.props.width); // if the height of the graph is set as proportional, change the height of the svg if ((this.options.height + '').indexOf('%') != -1 || this.updateSVGheightOnResize == true) { this.updateSVGheight = true; } } // update the height of the graph on each redraw of the graph. if (this.updateSVGheight == true) { if (this.options.graphHeight != this.props.height + 'px') { this.options.graphHeight = this.props.height + 'px'; this.svg.style.height = this.props.height + 'px'; } this.updateSVGheight = false; } else { this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px'; } // zoomed is here to ensure that animations are shown correctly. if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { resized = this._updateGraph() || resized; } else { // move the whole svg while dragging if (this.lastStart != 0) { var offset = this.body.range.start - this.lastStart; var range = this.body.range.end - this.body.range.start; if (this.props.width != 0) { var rangePerPixelInv = this.props.width / range; var xOffset = offset * rangePerPixelInv; this.svg.style.left = -this.props.width - xOffset + 'px'; } } } this.legendLeft.redraw(); this.legendRight.redraw(); return resized; }; /** * Update and redraw the graph. * */ LineGraph.prototype._updateGraph = function () { // reset the svg elements DOMutil.prepareElements(this.svgElements); if (this.props.width != 0 && this.itemsData != null) { var group, i; var preprocessedGroupData = {}; var processedGroupData = {}; var groupRanges = {}; var changeCalled = false; // getting group Ids var groupIds = []; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { group = this.groups[groupId]; if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) { groupIds.push(groupId); } } } if (groupIds.length > 0) { // this is the range of the SVG canvas var minDate = this.body.util.toGlobalTime(-this.body.domProps.root.width); var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width); var groupsData = {}; // fill groups data, this only loads the data we require based on the timewindow this._getRelevantData(groupIds, groupsData, minDate, maxDate); // apply sampling, if disabled, it will pass through this function. this._applySampling(groupIds, groupsData); // we transform the X coordinates to detect collisions for (i = 0; i < groupIds.length; i++) { preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); } // now all needed data has been collected we start the processing. this._getYRanges(groupIds, preprocessedGroupData, groupRanges); // update the Y axis first, we use this data to draw at the correct Y points // changeCalled is required to clean the SVG on a change emit. changeCalled = this._updateYAxis(groupIds, groupRanges); var MAX_CYCLES = 5; if (changeCalled == true && this.COUNTER < MAX_CYCLES) { DOMutil.cleanupElements(this.svgElements); this.abortedGraphUpdate = true; this.COUNTER++; this.body.emitter.emit('change'); return true; } else { if (this.COUNTER > MAX_CYCLES) { console.log('WARNING: there may be an infinite loop in the _updateGraph emitter cycle.'); } this.COUNTER = 0; this.abortedGraphUpdate = false; // With the yAxis scaled correctly, use this to get the Y values of the points. for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group); } // draw the groups for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; if (group.options.style != 'bar') { // bar needs to be drawn enmasse group.draw(processedGroupData[groupIds[i]], group, this.framework); } } BarFunctions.draw(groupIds, processedGroupData, this.framework); } } } // cleanup unused svg elements DOMutil.cleanupElements(this.svgElements); return false; }; /** * first select and preprocess the data from the datasets. * the groups have their preselection of data, we now loop over this data to see * what data we need to draw. Sorted data is much faster. * more optimization is possible by doing the sampling before and using the binary search * to find the end date to determine the increment. * * @param {array} groupIds * @param {object} groupsData * @param {date} minDate * @param {date} maxDate * @private */ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { var group, i, j, item; if (groupIds.length > 0) { for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; groupsData[groupIds[i]] = []; var dataContainer = groupsData[groupIds[i]]; // optimization for sorted data if (group.options.sort == true) { var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, 'x', 'before')); for (j = guess; j < group.itemsData.length; j++) { item = group.itemsData[j]; if (item !== undefined) { if (item.x > maxDate) { dataContainer.push(item); break; } else { dataContainer.push(item); } } } } else { for (j = 0; j < group.itemsData.length; j++) { item = group.itemsData[j]; if (item !== undefined) { if (item.x > minDate && item.x < maxDate) { dataContainer.push(item); } } } } } } }; /** * * @param groupIds * @param groupsData * @private */ LineGraph.prototype._applySampling = function (groupIds, groupsData) { var group; if (groupIds.length > 0) { for (var i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; if (group.options.sampling == true) { var dataContainer = groupsData[groupIds[i]]; if (dataContainer.length > 0) { var increment = 1; var amountOfPoints = dataContainer.length; // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop // of width changing of the yAxis. var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x); var pointsPerPixel = amountOfPoints / xDistance; increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel))); var sampledData = []; for (var j = 0; j < amountOfPoints; j += increment) { sampledData.push(dataContainer[j]); } groupsData[groupIds[i]] = sampledData; } } } } }; /** * * * @param {array} groupIds * @param {object} groupsData * @param {object} groupRanges | this is being filled here * @private */ LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { var groupData, group, i; var combinedDataLeft = []; var combinedDataRight = []; var options; if (groupIds.length > 0) { for (i = 0; i < groupIds.length; i++) { groupData = groupsData[groupIds[i]]; options = this.groups[groupIds[i]].options; if (groupData.length > 0) { group = this.groups[groupIds[i]]; // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. if (options.stack === true && options.style === 'bar') { if (options.yAxisOrientation === 'left') { combinedDataLeft = combinedDataLeft.concat(group.getData(groupData)); } else { combinedDataRight = combinedDataRight.concat(group.getData(groupData)); } } else { groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); } } } // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. BarFunctions.getStackedYRange(combinedDataLeft, groupRanges, groupIds, '__barStackLeft', 'left'); BarFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right'); // if line graphs are stacked, their range need to be handled differently and accumulated over all groups. //LineFunctions.getStackedYRange(combinedDataLeft , groupRanges, groupIds, '__lineStackLeft' , 'left' ); //LineFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__lineStackRight', 'right'); } }; /** * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden. * @param {Array} groupIds * @param {Object} groupRanges * @private */ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { var resized = false; var yAxisLeftUsed = false; var yAxisRightUsed = false; var minLeft = 1000000000, minRight = 1000000000, maxLeft = -1000000000, maxRight = -1000000000, minVal, maxVal; // if groups are present if (groupIds.length > 0) { // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. for (var i = 0; i < groupIds.length; i++) { var group = this.groups[groupIds[i]]; if (group && group.options.yAxisOrientation != 'right') { yAxisLeftUsed = true; minLeft = 0; maxLeft = 0; } else if (group && group.options.yAxisOrientation) { yAxisRightUsed = true; minRight = 0; maxRight = 0; } } // if there are items: for (var i = 0; i < groupIds.length; i++) { if (groupRanges.hasOwnProperty(groupIds[i])) { if (groupRanges[groupIds[i]].ignore !== true) { minVal = groupRanges[groupIds[i]].min; maxVal = groupRanges[groupIds[i]].max; if (groupRanges[groupIds[i]].yAxisOrientation != 'right') { yAxisLeftUsed = true; minLeft = minLeft > minVal ? minVal : minLeft; maxLeft = maxLeft < maxVal ? maxVal : maxLeft; } else { yAxisRightUsed = true; minRight = minRight > minVal ? minVal : minRight; maxRight = maxRight < maxVal ? maxVal : maxRight; } } } } if (yAxisLeftUsed == true) { this.yAxisLeft.setRange(minLeft, maxLeft); } if (yAxisRightUsed == true) { this.yAxisRight.setRange(minRight, maxRight); } } resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; if (yAxisRightUsed == true && yAxisLeftUsed == true) { this.yAxisLeft.drawIcons = true; this.yAxisRight.drawIcons = true; } else { this.yAxisLeft.drawIcons = false; this.yAxisRight.drawIcons = false; } this.yAxisRight.master = !yAxisLeftUsed; if (this.yAxisRight.master == false) { if (yAxisRightUsed == true) { this.yAxisLeft.lineOffset = this.yAxisRight.width; } else { this.yAxisLeft.lineOffset = 0; } resized = this.yAxisLeft.redraw() || resized; this.yAxisRight.stepPixels = this.yAxisLeft.stepPixels; this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; this.yAxisRight.amountOfSteps = this.yAxisLeft.amountOfSteps; resized = this.yAxisRight.redraw() || resized; } else { resized = this.yAxisRight.redraw() || resized; } // clean the accumulated lists var tempGroups = ['__barStackLeft', '__barStackRight', '__lineStackLeft', '__lineStackRight']; for (var i = 0; i < tempGroups.length; i++) { if (groupIds.indexOf(tempGroups[i]) != -1) { groupIds.splice(groupIds.indexOf(tempGroups[i]), 1); } } return resized; }; /** * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function * * @param {boolean} axisUsed * @returns {boolean} * @private * @param axis */ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { var changed = false; if (axisUsed == false) { if (axis.dom.frame.parentNode && axis.hidden == false) { axis.hide(); changed = true; } } else { if (!axis.dom.frame.parentNode && axis.hidden == true) { axis.show(); changed = true; } } return changed; }; /** * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for * the yAxis. * * @param datapoints * @returns {Array} * @private */ LineGraph.prototype._convertXcoordinates = function (datapoints) { var extractedData = []; var xValue, yValue; var toScreen = this.body.util.toScreen; for (var i = 0; i < datapoints.length; i++) { xValue = toScreen(datapoints[i].x) + this.props.width; yValue = datapoints[i].y; extractedData.push({ x: xValue, y: yValue }); } return extractedData; }; /** * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for * the yAxis. * * @param datapoints * @param group * @returns {Array} * @private */ LineGraph.prototype._convertYcoordinates = function (datapoints, group) { var extractedData = []; var xValue, yValue; var toScreen = this.body.util.toScreen; var axis = this.yAxisLeft; var svgHeight = Number(this.svg.style.height.replace('px', '')); if (group.options.yAxisOrientation == 'right') { axis = this.yAxisRight; } for (var i = 0; i < datapoints.length; i++) { var labelValue = datapoints[i].label ? datapoints[i].label : null; xValue = toScreen(datapoints[i].x) + this.props.width; yValue = Math.round(axis.convertValue(datapoints[i].y)); extractedData.push({ x: xValue, y: yValue, label: labelValue }); } group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); return extractedData; }; module.exports = LineGraph; /***/ }, /* 51 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var DOMutil = __webpack_require__(7); var Component = __webpack_require__(21); var DataStep = __webpack_require__(52); /** * A horizontal time axis * @param {Object} [options] See DataAxis.setOptions for the available * options. * @constructor DataAxis * @extends Component * @param body */ function DataAxis(body, options, svg, linegraphOptions) { this.id = util.randomUUID(); this.body = body; this.defaultOptions = { orientation: 'left', // supported: 'left', 'right' showMinorLabels: true, showMajorLabels: true, icons: true, majorLinesOffset: 7, minorLinesOffset: 4, labelOffsetX: 10, labelOffsetY: 2, iconWidth: 20, width: '40px', visible: true, alignZeros: true, left: { range: { min: undefined, max: undefined }, format: function format(value) { return value; }, title: { text: undefined, style: undefined } }, right: { range: { min: undefined, max: undefined }, format: function format(value) { return value; }, title: { text: undefined, style: undefined } } }; this.linegraphOptions = linegraphOptions; this.linegraphSVG = svg; this.props = {}; this.DOMelements = { // dynamic elements lines: {}, labels: {}, title: {} }; this.dom = {}; this.range = { start: 0, end: 0 }; this.options = util.extend({}, this.defaultOptions); this.conversionFactor = 1; this.setOptions(options); this.width = Number(('' + this.options.width).replace('px', '')); this.minWidth = this.width; this.height = this.linegraphSVG.offsetHeight; this.hidden = false; this.stepPixels = 25; this.zeroCrossing = -1; this.amountOfSteps = -1; this.lineOffset = 0; this.master = true; this.svgElements = {}; this.iconsRemoved = false; this.groups = {}; this.amountOfGroups = 0; // create the HTML DOM this._create(); var me = this; this.body.emitter.on('verticalDrag', function () { me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px'; }); } DataAxis.prototype = new Component(); DataAxis.prototype.addGroup = function (label, graphOptions) { if (!this.groups.hasOwnProperty(label)) { this.groups[label] = graphOptions; } this.amountOfGroups += 1; }; DataAxis.prototype.updateGroup = function (label, graphOptions) { this.groups[label] = graphOptions; }; DataAxis.prototype.removeGroup = function (label) { if (this.groups.hasOwnProperty(label)) { delete this.groups[label]; this.amountOfGroups -= 1; } }; DataAxis.prototype.setOptions = function (options) { if (options) { var redraw = false; if (this.options.orientation != options.orientation && options.orientation !== undefined) { redraw = true; } var fields = ['orientation', 'showMinorLabels', 'showMajorLabels', 'icons', 'majorLinesOffset', 'minorLinesOffset', 'labelOffsetX', 'labelOffsetY', 'iconWidth', 'width', 'visible', 'left', 'right', 'alignZeros']; util.selectiveExtend(fields, this.options, options); this.minWidth = Number(('' + this.options.width).replace('px', '')); if (redraw === true && this.dom.frame) { this.hide(); this.show(); } } }; /** * Create the HTML DOM for the DataAxis */ DataAxis.prototype._create = function () { this.dom.frame = document.createElement('div'); this.dom.frame.style.width = this.options.width; this.dom.frame.style.height = this.height; this.dom.lineContainer = document.createElement('div'); this.dom.lineContainer.style.width = '100%'; this.dom.lineContainer.style.height = this.height; this.dom.lineContainer.style.position = 'relative'; // create svg element for graph drawing. this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svg.style.position = 'absolute'; this.svg.style.top = '0px'; this.svg.style.height = '100%'; this.svg.style.width = '100%'; this.svg.style.display = 'block'; this.dom.frame.appendChild(this.svg); }; DataAxis.prototype._redrawGroupIcons = function () { DOMutil.prepareElements(this.svgElements); var x; var iconWidth = this.options.iconWidth; var iconHeight = 15; var iconOffset = 4; var y = iconOffset + 0.5 * iconHeight; if (this.options.orientation === 'left') { x = iconOffset; } else { x = this.width - iconWidth - iconOffset; } var groupArray = Object.keys(this.groups); groupArray.sort(function (a, b) { return a < b ? -1 : 1; }); for (var i = 0; i < groupArray.length; i++) { var groupId = groupArray[i]; if (this.groups[groupId].visible === true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] === true)) { this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); y += iconHeight + iconOffset; } } DOMutil.cleanupElements(this.svgElements); this.iconsRemoved = false; }; DataAxis.prototype._cleanupIcons = function () { if (this.iconsRemoved === false) { DOMutil.prepareElements(this.svgElements); DOMutil.cleanupElements(this.svgElements); this.iconsRemoved = true; } }; /** * Create the HTML DOM for the DataAxis */ DataAxis.prototype.show = function () { this.hidden = false; if (!this.dom.frame.parentNode) { if (this.options.orientation === 'left') { this.body.dom.left.appendChild(this.dom.frame); } else { this.body.dom.right.appendChild(this.dom.frame); } } if (!this.dom.lineContainer.parentNode) { this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); } }; /** * Create the HTML DOM for the DataAxis */ DataAxis.prototype.hide = function () { this.hidden = true; if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); } if (this.dom.lineContainer.parentNode) { this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); } }; /** * Set a range (start and end) * @param end * @param start * @param end */ DataAxis.prototype.setRange = function (start, end) { if (this.master === false && this.options.alignZeros === true && this.zeroCrossing != -1) { if (start > 0) { start = 0; } } this.range.start = start; this.range.end = end; }; /** * Repaint the component * @return {boolean} Returns true if the component is resized */ DataAxis.prototype.redraw = function () { var resized = false; var activeGroups = 0; // Make sure the line container adheres to the vertical scrolling. this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px'; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { if (this.groups[groupId].visible === true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] === true)) { activeGroups++; } } } if (this.amountOfGroups === 0 || activeGroups === 0) { this.hide(); } else { this.show(); this.height = Number(this.linegraphSVG.style.height.replace('px', '')); // svg offsetheight did not work in firefox and explorer... this.dom.lineContainer.style.height = this.height + 'px'; this.width = this.options.visible === true ? Number(('' + this.options.width).replace('px', '')) : 0; var props = this.props; var frame = this.dom.frame; // update classname frame.className = 'vis-data-axis'; // calculate character width and height this._calculateCharSize(); var orientation = this.options.orientation; var showMinorLabels = this.options.showMinorLabels; var showMajorLabels = this.options.showMajorLabels; // determine the width and height of the elements for the axis props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset; props.minorLineHeight = 1; props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset; props.majorLineHeight = 1; // take frame offline while updating (is almost twice as fast) if (orientation === 'left') { frame.style.top = '0'; frame.style.left = '0'; frame.style.bottom = ''; frame.style.width = this.width + 'px'; frame.style.height = this.height + 'px'; this.props.width = this.body.domProps.left.width; this.props.height = this.body.domProps.left.height; } else { // right frame.style.top = ''; frame.style.bottom = '0'; frame.style.left = '0'; frame.style.width = this.width + 'px'; frame.style.height = this.height + 'px'; this.props.width = this.body.domProps.right.width; this.props.height = this.body.domProps.right.height; } resized = this._redrawLabels(); resized = this._isResized() || resized; if (this.options.icons === true) { this._redrawGroupIcons(); } else { this._cleanupIcons(); } this._redrawTitle(orientation); } return resized; }; /** * Repaint major and minor text labels and vertical grid lines * @private */ DataAxis.prototype._redrawLabels = function () { var resized = false; DOMutil.prepareElements(this.DOMelements.lines); DOMutil.prepareElements(this.DOMelements.labels); var orientation = this.options['orientation']; // get the range for the slaved axis var step; if (this.master === false) { var stepSize, rangeStart, rangeEnd, minimumStep; if (this.zeroCrossing !== -1 && this.options.alignZeros === true) { if (this.range.end > 0) { stepSize = this.range.end / this.zeroCrossing; // size of one step rangeStart = this.range.end - this.amountOfSteps * stepSize; rangeEnd = this.range.end; } else { // all of the range (including start) has to be done before the zero crossing. stepSize = -1 * this.range.start / (this.amountOfSteps - this.zeroCrossing); // absolute size of a step rangeStart = this.range.start; rangeEnd = this.range.start + stepSize * this.amountOfSteps; } } else { rangeStart = this.range.start; rangeEnd = this.range.end; } minimumStep = this.stepPixels; } else { // calculate range and step (step such that we have space for 7 characters per label) minimumStep = this.props.majorCharHeight; rangeStart = this.range.start; rangeEnd = this.range.end; } this.step = step = new DataStep(rangeStart, rangeEnd, minimumStep, this.dom.frame.offsetHeight, this.options[this.options.orientation].range, this.options[this.options.orientation].format, this.master === false && this.options.alignZeros // does the step have to align zeros? only if not master and the options is on ); // the slave axis needs to use the same horizontal lines as the master axis. if (this.master === true) { this.stepPixels = this.dom.frame.offsetHeight / step.marginRange * step.step; this.amountOfSteps = Math.ceil(this.dom.frame.offsetHeight / this.stepPixels); } else { // align with zero if (this.options.alignZeros === true && this.zeroCrossing !== -1) { // distance is the amount of steps away from the zero crossing we are. var distance = (step.current - this.zeroCrossing * step.step) / step.step; this.step.shift(distance); } } // value at the bottom of the SVG this.valueAtBottom = step.marginEnd; this.maxLabelSize = 0; var y = 0; // init value var stepIndex = 0; // init value var isMajor = false; // init value while (stepIndex < this.amountOfSteps) { y = Math.round(stepIndex * this.stepPixels); isMajor = step.isMajor(); if (stepIndex > 0 && stepIndex !== this.amountOfSteps) { if (this.options['showMinorLabels'] && isMajor === false || this.master === false && this.options['showMinorLabels'] === true) { this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight); } if (isMajor && this.options['showMajorLabels'] && this.master === true || this.options['showMinorLabels'] === false && this.master === false && isMajor === true) { if (y >= 0) { this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-major', this.props.majorCharHeight); } this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth); } else { this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth); } } // get zero crossing if (this.master === true && step.current === 0) { this.zeroCrossing = stepIndex; } step.next(); stepIndex += 1; } // get zero crossing if it's the last step if (this.master === true && step.current === 0) { this.zeroCrossing = stepIndex; } this.conversionFactor = this.stepPixels / step.step; // Note that title is rotated, so we're using the height, not width! var titleWidth = 0; if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) { titleWidth = this.props.titleCharHeight; } var offset = this.options.icons === true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15; // this will resize the yAxis to accommodate the labels. if (this.maxLabelSize > this.width - offset && this.options.visible === true) { this.width = this.maxLabelSize + offset; this.options.width = this.width + 'px'; DOMutil.cleanupElements(this.DOMelements.lines); DOMutil.cleanupElements(this.DOMelements.labels); this.redraw(); resized = true; } // this will resize the yAxis if it is too big for the labels. else if (this.maxLabelSize < this.width - offset && this.options.visible === true && this.width > this.minWidth) { this.width = Math.max(this.minWidth, this.maxLabelSize + offset); this.options.width = this.width + 'px'; DOMutil.cleanupElements(this.DOMelements.lines); DOMutil.cleanupElements(this.DOMelements.labels); this.redraw(); resized = true; } else { DOMutil.cleanupElements(this.DOMelements.lines); DOMutil.cleanupElements(this.DOMelements.labels); resized = false; } return resized; }; DataAxis.prototype.convertValue = function (value) { var invertedValue = this.valueAtBottom - value; var convertedValue = invertedValue * this.conversionFactor; return convertedValue; }; DataAxis.prototype.screenToValue = function (x) { return this.valueAtBottom - x / this.conversionFactor; }; /** * Create a label for the axis at position x * @private * @param y * @param text * @param orientation * @param className * @param characterHeight */ DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) { // reuse redundant label var label = DOMutil.getDOMElement('div', this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift(); label.className = className; label.innerHTML = text; if (orientation === 'left') { label.style.left = '-' + this.options.labelOffsetX + 'px'; label.style.textAlign = 'right'; } else { label.style.right = '-' + this.options.labelOffsetX + 'px'; label.style.textAlign = 'left'; } label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px'; text += ''; var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); if (this.maxLabelSize < text.length * largestWidth) { this.maxLabelSize = text.length * largestWidth; } }; /** * Create a minor line for the axis at position y * @param y * @param orientation * @param className * @param offset * @param width */ DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) { if (this.master === true) { var line = DOMutil.getDOMElement('div', this.DOMelements.lines, this.dom.lineContainer); //this.dom.redundant.lines.shift(); line.className = className; line.innerHTML = ''; if (orientation === 'left') { line.style.left = this.width - offset + 'px'; } else { line.style.right = this.width - offset + 'px'; } line.style.width = width + 'px'; line.style.top = y + 'px'; } }; /** * Create a title for the axis * @private * @param orientation */ DataAxis.prototype._redrawTitle = function (orientation) { DOMutil.prepareElements(this.DOMelements.title); // Check if the title is defined for this axes if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) { var title = DOMutil.getDOMElement('div', this.DOMelements.title, this.dom.frame); title.className = 'vis-y-axis vis-title vis-' + orientation; title.innerHTML = this.options[orientation].title.text; // Add style - if provided if (this.options[orientation].title.style !== undefined) { util.addCssText(title, this.options[orientation].title.style); } if (orientation === 'left') { title.style.left = this.props.titleCharHeight + 'px'; } else { title.style.right = this.props.titleCharHeight + 'px'; } title.style.width = this.height + 'px'; } // we need to clean up in case we did not use all elements. DOMutil.cleanupElements(this.DOMelements.title); }; /** * Determine the size of text on the axis (both major and minor axis). * The size is calculated only once and then cached in this.props. * @private */ DataAxis.prototype._calculateCharSize = function () { // determine the char width and height on the minor axis if (!('minorCharHeight' in this.props)) { var textMinor = document.createTextNode('0'); var measureCharMinor = document.createElement('div'); measureCharMinor.className = 'vis-y-axis vis-minor vis-measure'; measureCharMinor.appendChild(textMinor); this.dom.frame.appendChild(measureCharMinor); this.props.minorCharHeight = measureCharMinor.clientHeight; this.props.minorCharWidth = measureCharMinor.clientWidth; this.dom.frame.removeChild(measureCharMinor); } if (!('majorCharHeight' in this.props)) { var textMajor = document.createTextNode('0'); var measureCharMajor = document.createElement('div'); measureCharMajor.className = 'vis-y-axis vis-major vis-measure'; measureCharMajor.appendChild(textMajor); this.dom.frame.appendChild(measureCharMajor); this.props.majorCharHeight = measureCharMajor.clientHeight; this.props.majorCharWidth = measureCharMajor.clientWidth; this.dom.frame.removeChild(measureCharMajor); } if (!('titleCharHeight' in this.props)) { var textTitle = document.createTextNode('0'); var measureCharTitle = document.createElement('div'); measureCharTitle.className = 'vis-y-axis vis-title vis-measure'; measureCharTitle.appendChild(textTitle); this.dom.frame.appendChild(measureCharTitle); this.props.titleCharHeight = measureCharTitle.clientHeight; this.props.titleCharWidth = measureCharTitle.clientWidth; this.dom.frame.removeChild(measureCharTitle); } }; module.exports = DataAxis; /***/ }, /* 52 */ /***/ function(module, exports) { /** * @constructor DataStep * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an * end data point. The class itself determines the best scale (step size) based on the * provided start Date, end Date, and minimumStep. * * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * * Alternatively, you can set a scale by hand. * After creation, you can initialize the class by executing first(). Then you * can iterate from the start date to the end date via next(). You can check if * the end date is reached with the function hasNext(). After each step, you can * retrieve the current date via getCurrent(). * The DataStep has scales ranging from milliseconds, seconds, minutes, hours, * days, to years. * * Version: 1.2 * * @param {Date} [start] The start date, for example new Date(2010, 9, 21) * or new Date(2010, 9, 21, 23, 45, 00) * @param {Date} [end] The end date * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ 'use strict'; function DataStep(start, end, minimumStep, containerHeight, customRange, formattingFunction, alignZeros) { // variables this.current = 0; this.autoScale = true; this.stepIndex = 0; this.step = 1; this.scale = 1; this.formattingFunction = formattingFunction; this.marginStart; this.marginEnd; this.deadSpace = 0; this.majorSteps = [1, 2, 5, 10]; this.minorSteps = [0.25, 0.5, 1, 2]; this.alignZeros = alignZeros; this.setRange(start, end, minimumStep, containerHeight, customRange); } /** * Set a new range * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * @param {Number} [start] The start date and time. * @param {Number} [end] The end date and time. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ DataStep.prototype.setRange = function (start, end, minimumStep, containerHeight, customRange) { this._start = customRange.min === undefined ? start : customRange.min; this._end = customRange.max === undefined ? end : customRange.max; if (this._start === this._end) { this._start = customRange.min === undefined ? this._start - 0.75 : this._start; this._end = customRange.max === undefined ? this._end + 1 : this._end;; } if (this.autoScale === true) { this.setMinimumStep(minimumStep, containerHeight); } this.setFirst(customRange); }; /** * Automatically determine the scale that bests fits the provided minimum step * @param {Number} [minimumStep] The minimum step size in pixels */ DataStep.prototype.setMinimumStep = function (minimumStep, containerHeight) { // round to floor var range = this._end - this._start; var safeRange = range * 1.2; var minimumStepValue = minimumStep * (safeRange / containerHeight); var orderOfMagnitude = Math.round(Math.log(safeRange) / Math.LN10); var minorStepIdx = -1; var magnitudefactor = Math.pow(10, orderOfMagnitude); var start = 0; if (orderOfMagnitude < 0) { start = orderOfMagnitude; } var solutionFound = false; for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { magnitudefactor = Math.pow(10, i); for (var j = 0; j < this.minorSteps.length; j++) { var stepSize = magnitudefactor * this.minorSteps[j]; if (stepSize >= minimumStepValue) { solutionFound = true; minorStepIdx = j; break; } } if (solutionFound === true) { break; } } this.stepIndex = minorStepIdx; this.scale = magnitudefactor; this.step = magnitudefactor * this.minorSteps[minorStepIdx]; }; /** * Round the current date to the first minor date value * This must be executed once when the current date is set to start Date */ DataStep.prototype.setFirst = function (customRange) { if (customRange === undefined) { customRange = {}; } var niceStart = customRange.min === undefined ? this._start - this.scale * 2 * this.minorSteps[this.stepIndex] : customRange.min; var niceEnd = customRange.max === undefined ? this._end + this.scale * this.minorSteps[this.stepIndex] : customRange.max; this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; // if we need to align the zero's we need to make sure that there is a zero to use. if (this.alignZeros === true && (this.marginEnd - this.marginStart) % this.step != 0) { this.marginEnd += this.marginEnd % this.step; } this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; this.marginRange = this.marginEnd - this.marginStart; this.current = this.marginEnd; }; DataStep.prototype.roundToMinor = function (value) { var rounded = value - value % (this.scale * this.minorSteps[this.stepIndex]); if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { return rounded + this.scale * this.minorSteps[this.stepIndex]; } else { return rounded; } }; /** * Check if the there is a next step * @return {boolean} true if the current date has not passed the end date */ DataStep.prototype.hasNext = function () { return this.current >= this.marginStart; }; /** * Do the next step */ DataStep.prototype.next = function () { var prev = this.current; this.current -= this.step; // safety mechanism: if current time is still unchanged, move to the end if (this.current === prev) { this.current = this._end; } }; /** * Do the next step */ DataStep.prototype.previous = function () { this.current += this.step; this.marginEnd += this.step; this.marginRange = this.marginEnd - this.marginStart; }; /** * Get the current datetime * @return {String} current The current date */ DataStep.prototype.getCurrent = function () { // prevent round-off errors when close to zero var current = Math.abs(this.current) < this.step / 2 ? 0 : this.current; var returnValue = current.toPrecision(5); if (typeof this.formattingFunction === 'function') { returnValue = this.formattingFunction(current); } if (typeof returnValue === 'number') { return '' + returnValue; } else if (typeof returnValue === 'string') { return returnValue; } else { return current.toPrecision(5); } }; /** * Check if the current value is a major value (for example when the step * is DAY, a major value is each first day of the MONTH) * @return {boolean} true if current date is major, else false. */ DataStep.prototype.isMajor = function () { return this.current % (this.scale * this.majorSteps[this.stepIndex]) === 0; }; DataStep.prototype.shift = function (steps) { if (steps < 0) { for (var i = 0; i < -steps; i++) { this.previous(); } } else if (steps > 0) { for (var i = 0; i < steps; i++) { this.next(); } } }; module.exports = DataStep; /***/ }, /* 53 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var DOMutil = __webpack_require__(7); var Line = __webpack_require__(54); var Bar = __webpack_require__(56); var Points = __webpack_require__(55); /** * /** * @param {object} group | the object of the group from the dataset * @param {string} groupId | ID of the group * @param {object} options | the default options * @param {array} groupsUsingDefaultStyles | this array has one entree. * It is passed as an array so it is passed by reference. * It enumerates through the default styles * @constructor */ function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { this.id = groupId; var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation']; this.options = util.selectiveBridgeObject(fields, options); this.usingDefaultStyle = group.className === undefined; this.groupsUsingDefaultStyles = groupsUsingDefaultStyles; this.zeroPosition = 0; this.update(group); if (this.usingDefaultStyle == true) { this.groupsUsingDefaultStyles[0] += 1; } this.itemsData = []; this.visible = group.visible === undefined ? true : group.visible; } /** * this loads a reference to all items in this group into this group. * @param {array} items */ GraphGroup.prototype.setItems = function (items) { if (items != null) { this.itemsData = items; if (this.options.sort == true) { this.itemsData.sort(function (a, b) { return a.x - b.x; }); } } else { this.itemsData = []; } }; /** * this is used for plotting barcharts, this way, we only have to calculate it once. * @param pos */ GraphGroup.prototype.setZeroPosition = function (pos) { this.zeroPosition = pos; }; /** * set the options of the graph group over the default options. * @param options */ GraphGroup.prototype.setOptions = function (options) { if (options !== undefined) { var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart']; util.selectiveDeepExtend(fields, this.options, options); util.mergeOptions(this.options, options, 'interpolation'); util.mergeOptions(this.options, options, 'drawPoints'); util.mergeOptions(this.options, options, 'shaded'); if (options.interpolation) { if (typeof options.interpolation == 'object') { if (options.interpolation.parametrization) { if (options.interpolation.parametrization == 'uniform') { this.options.interpolation.alpha = 0; } else if (options.interpolation.parametrization == 'chordal') { this.options.interpolation.alpha = 1; } else { this.options.interpolation.parametrization = 'centripetal'; this.options.interpolation.alpha = 0.5; } } } } } if (this.options.style == 'line') { this.type = new Line(this.id, this.options); } else if (this.options.style == 'bar') { this.type = new Bar(this.id, this.options); } else if (this.options.style == 'points') { this.type = new Points(this.id, this.options); } }; /** * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph * @param group */ GraphGroup.prototype.update = function (group) { this.group = group; this.content = group.content || 'graph'; this.className = group.className || this.className || 'vis-graph-group' + this.groupsUsingDefaultStyles[0] % 10; this.visible = group.visible === undefined ? true : group.visible; this.style = group.style; this.setOptions(group.options); }; /** * draw the icon for the legend. * * @param x * @param y * @param JSONcontainer * @param SVGcontainer * @param iconWidth * @param iconHeight */ GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { var fillHeight = iconHeight * 0.5; var path, fillPath; var outline = DOMutil.getSVGElement('rect', JSONcontainer, SVGcontainer); outline.setAttributeNS(null, 'x', x); outline.setAttributeNS(null, 'y', y - fillHeight); outline.setAttributeNS(null, 'width', iconWidth); outline.setAttributeNS(null, 'height', 2 * fillHeight); outline.setAttributeNS(null, 'class', 'vis-outline'); if (this.options.style == 'line') { path = DOMutil.getSVGElement('path', JSONcontainer, SVGcontainer); path.setAttributeNS(null, 'class', this.className); if (this.style !== undefined) { path.setAttributeNS(null, 'style', this.style); } path.setAttributeNS(null, 'd', 'M' + x + ',' + y + ' L' + (x + iconWidth) + ',' + y + ''); if (this.options.shaded.enabled == true) { fillPath = DOMutil.getSVGElement('path', JSONcontainer, SVGcontainer); if (this.options.shaded.orientation == 'top') { fillPath.setAttributeNS(null, 'd', 'M' + x + ', ' + (y - fillHeight) + 'L' + x + ',' + y + ' L' + (x + iconWidth) + ',' + y + ' L' + (x + iconWidth) + ',' + (y - fillHeight)); } else { fillPath.setAttributeNS(null, 'd', 'M' + x + ',' + y + ' ' + 'L' + x + ',' + (y + fillHeight) + ' ' + 'L' + (x + iconWidth) + ',' + (y + fillHeight) + 'L' + (x + iconWidth) + ',' + y); } fillPath.setAttributeNS(null, 'class', this.className + ' vis-icon-fill'); } if (this.options.drawPoints.enabled == true) { DOMutil.drawPoint(x + 0.5 * iconWidth, y, this, JSONcontainer, SVGcontainer); } } else { var barWidth = Math.round(0.3 * iconWidth); var bar1Height = Math.round(0.4 * iconHeight); var bar2Height = Math.round(0.75 * iconHeight); var offset = Math.round((iconWidth - 2 * barWidth) / 3); DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style); DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style); } }; /** * return the legend entree for this group. * * @param iconWidth * @param iconHeight * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} */ GraphGroup.prototype.getLegend = function (iconWidth, iconHeight) { var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.drawIcon(0, 0.5 * iconHeight, [], svg, iconWidth, iconHeight); return { icon: svg, label: this.content, orientation: this.options.yAxisOrientation }; }; GraphGroup.prototype.getYRange = function (groupData) { return this.type.getYRange(groupData); }; GraphGroup.prototype.getData = function (groupData) { return this.type.getData(groupData); }; GraphGroup.prototype.draw = function (dataset, group, framework) { this.type.draw(dataset, group, framework); }; module.exports = GraphGroup; /***/ }, /* 54 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var DOMutil = __webpack_require__(7); var Points = __webpack_require__(55); function Line(groupId, options) { this.groupId = groupId; this.options = options; } Line.prototype.getData = function (groupData) { var combinedData = []; for (var j = 0; j < groupData.length; j++) { combinedData.push({ x: groupData[j].x, y: groupData[j].y, groupId: this.groupId }); } return combinedData; }; Line.prototype.getYRange = function (groupData) { var yMin = groupData[0].y; var yMax = groupData[0].y; for (var j = 0; j < groupData.length; j++) { yMin = yMin > groupData[j].y ? groupData[j].y : yMin; yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; Line.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { if (combinedData.length > 0) { // sort by time and by group combinedData.sort(function (a, b) { if (a.x === b.x) { return a.groupId < b.groupId ? -1 : 1; } else { return a.x - b.x; } }); var intersections = {}; Line._getDataIntersections(intersections, combinedData); groupRanges[groupLabel] = Line._getStackedYRange(intersections, combinedData); groupRanges[groupLabel].yAxisOrientation = orientation; groupIds.push(groupLabel); } }; Line._getStackedYRange = function (intersections, combinedData) { var key; var yMin = combinedData[0].y; var yMax = combinedData[0].y; for (var i = 0; i < combinedData.length; i++) { key = combinedData[i].x; if (intersections[key] === undefined) { yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; } else { if (combinedData[i].y < 0) { intersections[key].accumulatedNegative += combinedData[i].y; } else { intersections[key].accumulatedPositive += combinedData[i].y; } } } for (var xpos in intersections) { if (intersections.hasOwnProperty(xpos)) { yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; } } return { min: yMin, max: yMax }; }; /** * Fill the intersections object with counters of how many datapoints share the same x coordinates * @param intersections * @param combinedData * @private */ Line._getDataIntersections = function (intersections, combinedData) { // get intersections var coreDistance; for (var i = 0; i < combinedData.length; i++) { if (i + 1 < combinedData.length) { coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); } if (i > 0) { coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); } if (coreDistance === 0) { if (intersections[combinedData[i].x] === undefined) { intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; } intersections[combinedData[i].x].amount += 1; } } }; /** * draw a line graph * * @param dataset * @param group */ Line.prototype.draw = function (dataset, group, framework) { if (dataset != null) { if (dataset.length > 0) { var path, d; var svgHeight = Number(framework.svg.style.height.replace('px', '')); path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); path.setAttributeNS(null, 'class', group.className); if (group.style !== undefined) { path.setAttributeNS(null, 'style', group.style); } // construct path from dataset if (group.options.interpolation.enabled == true) { d = Line._catmullRom(dataset, group); } else { d = Line._linear(dataset); } // append with points for fill and finalize the path if (group.options.shaded.enabled == true) { var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); var dFill; if (group.options.shaded.orientation == 'top') { dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0; } else { dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight; } fillPath.setAttributeNS(null, 'class', group.className + ' vis-fill'); if (group.options.shaded.style !== undefined) { fillPath.setAttributeNS(null, 'style', group.options.shaded.style); } fillPath.setAttributeNS(null, 'd', dFill); } // copy properties to path for drawing. path.setAttributeNS(null, 'd', 'M' + d); // draw points if (group.options.drawPoints.enabled == true) { Points.draw(dataset, group, framework); } } } }; /** * This uses an uniform parametrization of the interpolation algorithm: * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al. * @param data * @returns {string} * @private */ Line._catmullRomUniform = function (data) { // catmull rom var p0, p1, p2, p3, bp1, bp2; var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; var normalization = 1 / 6; var length = data.length; for (var i = 0; i < length - 1; i++) { p0 = i == 0 ? data[0] : data[i - 1]; p1 = data[i]; p2 = data[i + 1]; p3 = i + 2 < length ? data[i + 2] : p2; // Catmull-Rom to Cubic Bezier conversion matrix // 0 1 0 0 // -1/6 1 1/6 0 // 0 1/6 1 -1/6 // 0 0 1 0 // bp0 = { x: p1.x, y: p1.y }; bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; // bp0 = { x: p2.x, y: p2.y }; d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; } return d; }; /** * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. * By default, the centripetal parameterization is used because this gives the nicest results. * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. * * One optimization can be used to reuse distances since this is a sliding window approach. * @param data * @param group * @returns {string} * @private */ Line._catmullRom = function (data, group) { var alpha = group.options.interpolation.alpha; if (alpha == 0 || alpha === undefined) { return this._catmullRomUniform(data); } else { var p0, p1, p2, p3, bp1, bp2, d1, d2, d3, A, B, N, M; var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA; var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; var length = data.length; for (var i = 0; i < length - 1; i++) { p0 = i == 0 ? data[0] : data[i - 1]; p1 = data[i]; p2 = data[i + 1]; p3 = i + 2 < length ? data[i + 2] : p2; d1 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); d2 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); d3 = Math.sqrt(Math.pow(p2.x - p3.x, 2) + Math.pow(p2.y - p3.y, 2)); // Catmull-Rom to Cubic Bezier conversion matrix // A = 2d1^2a + 3d1^a * d2^a + d3^2a // B = 2d3^2a + 3d3^a * d2^a + d2^2a // [ 0 1 0 0 ] // [ -d2^2a /N A/N d1^2a /N 0 ] // [ 0 d3^2a /M B/M -d2^2a /M ] // [ 0 0 1 0 ] d3powA = Math.pow(d3, alpha); d3pow2A = Math.pow(d3, 2 * alpha); d2powA = Math.pow(d2, alpha); d2pow2A = Math.pow(d2, 2 * alpha); d1powA = Math.pow(d1, alpha); d1pow2A = Math.pow(d1, 2 * alpha); A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A; B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A; N = 3 * d1powA * (d1powA + d2powA); if (N > 0) { N = 1 / N; } M = 3 * d3powA * (d3powA + d2powA); if (M > 0) { M = 1 / M; } bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; if (bp1.x == 0 && bp1.y == 0) { bp1 = p1; } if (bp2.x == 0 && bp2.y == 0) { bp2 = p2; } d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; } return d; } }; /** * this generates the SVG path for a linear drawing between datapoints. * @param data * @returns {string} * @private */ Line._linear = function (data) { // linear var d = ''; for (var i = 0; i < data.length; i++) { if (i == 0) { d += data[i].x + ',' + data[i].y; } else { d += ' ' + data[i].x + ',' + data[i].y; } } return d; }; module.exports = Line; /***/ }, /* 55 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var DOMutil = __webpack_require__(7); function Points(groupId, options) { this.groupId = groupId; this.options = options; } Points.prototype.getYRange = function (groupData) { var yMin = groupData[0].y; var yMax = groupData[0].y; for (var j = 0; j < groupData.length; j++) { yMin = yMin > groupData[j].y ? groupData[j].y : yMin; yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; Points.prototype.draw = function (dataset, group, framework, offset) { Points.draw(dataset, group, framework, offset); }; /** * draw the data points * * @param {Array} dataset * @param {Object} JSONcontainer * @param {Object} svg | SVG DOM element * @param {GraphGroup} group * @param {Number} [offset] */ Points.draw = function (dataset, group, framework, offset) { if (offset === undefined) { offset = 0; } for (var i = 0; i < dataset.length; i++) { DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label); } }; module.exports = Points; /***/ }, /* 56 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var DOMutil = __webpack_require__(7); var Points = __webpack_require__(55); function Bargraph(groupId, options) { this.groupId = groupId; this.options = options; } Bargraph.prototype.getYRange = function (groupData) { var yMin = groupData[0].y; var yMax = groupData[0].y; for (var j = 0; j < groupData.length; j++) { yMin = yMin > groupData[j].y ? groupData[j].y : yMin; yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; Bargraph.prototype.getData = function (groupData) { var combinedData = []; for (var j = 0; j < groupData.length; j++) { combinedData.push({ x: groupData[j].x, y: groupData[j].y, groupId: this.groupId }); } return combinedData; }; /** * draw a bar graph * * @param groupIds * @param processedGroupData */ Bargraph.draw = function (groupIds, processedGroupData, framework) { var combinedData = []; var intersections = {}; var coreDistance; var key, drawData; var group; var i, j; var barPoints = 0; // combine all barchart data for (i = 0; i < groupIds.length; i++) { group = framework.groups[groupIds[i]]; if (group.options.style === 'bar') { if (group.visible === true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] === true)) { for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { combinedData.push({ x: processedGroupData[groupIds[i]][j].x, y: processedGroupData[groupIds[i]][j].y, groupId: groupIds[i], label: processedGroupData[groupIds[i]][j].label }); barPoints += 1; } } } } if (barPoints === 0) { return; } // sort by time and by group combinedData.sort(function (a, b) { if (a.x === b.x) { return a.groupId < b.groupId ? -1 : 1; } else { return a.x - b.x; } }); // get intersections Bargraph._getDataIntersections(intersections, combinedData); // plot barchart for (i = 0; i < combinedData.length; i++) { group = framework.groups[combinedData[i].groupId]; var minWidth = 0.1 * group.options.barChart.width; key = combinedData[i].x; var heightOffset = 0; if (intersections[key] === undefined) { if (i + 1 < combinedData.length) { coreDistance = Math.abs(combinedData[i + 1].x - key); } if (i > 0) { coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); } drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); } else { var nextKey = i + (intersections[key].amount - intersections[key].resolved); var prevKey = i - (intersections[key].resolved + 1); if (nextKey < combinedData.length) { coreDistance = Math.abs(combinedData[nextKey].x - key); } if (prevKey > 0) { coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); } drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); intersections[key].resolved += 1; if (group.options.stack === true) { if (combinedData[i].y < group.zeroPosition) { heightOffset = intersections[key].accumulatedNegative; intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].y; } else { heightOffset = intersections[key].accumulatedPositive; intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].y; } } else if (group.options.barChart.sideBySide === true) { drawData.width = drawData.width / intersections[key].amount; drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); if (group.options.barChart.align === 'left') { drawData.offset -= 0.5 * drawData.width; } else if (group.options.barChart.align === 'right') { drawData.offset += 0.5 * drawData.width; } } } DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style); // draw points if (group.options.drawPoints.enabled === true) { Points.draw([combinedData[i]], group, framework, drawData.offset); //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); } } }; /** * Fill the intersections object with counters of how many datapoints share the same x coordinates * @param intersections * @param combinedData * @private */ Bargraph._getDataIntersections = function (intersections, combinedData) { // get intersections var coreDistance; for (var i = 0; i < combinedData.length; i++) { if (i + 1 < combinedData.length) { coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); } if (i > 0) { coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); } if (coreDistance === 0) { if (intersections[combinedData[i].x] === undefined) { intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; } intersections[combinedData[i].x].amount += 1; } } }; /** * Get the width and offset for bargraphs based on the coredistance between datapoints * * @param coreDistance * @param group * @param minWidth * @returns {{width: Number, offset: Number}} * @private */ Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { var width, offset; if (coreDistance < group.options.barChart.width && coreDistance > 0) { width = coreDistance < minWidth ? minWidth : coreDistance; offset = 0; // recalculate offset with the new width; if (group.options.barChart.align === 'left') { offset -= 0.5 * coreDistance; } else if (group.options.barChart.align === 'right') { offset += 0.5 * coreDistance; } } else { // default settings width = group.options.barChart.width; offset = 0; if (group.options.barChart.align === 'left') { offset -= 0.5 * group.options.barChart.width; } else if (group.options.barChart.align === 'right') { offset += 0.5 * group.options.barChart.width; } } return { width: width, offset: offset }; }; Bargraph.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { if (combinedData.length > 0) { // sort by time and by group combinedData.sort(function (a, b) { if (a.x === b.x) { return a.groupId < b.groupId ? -1 : 1; } else { return a.x - b.x; } }); var intersections = {}; Bargraph._getDataIntersections(intersections, combinedData); groupRanges[groupLabel] = Bargraph._getStackedYRange(intersections, combinedData); groupRanges[groupLabel].yAxisOrientation = orientation; groupIds.push(groupLabel); } }; Bargraph._getStackedYRange = function (intersections, combinedData) { var key; var yMin = combinedData[0].y; var yMax = combinedData[0].y; for (var i = 0; i < combinedData.length; i++) { key = combinedData[i].x; if (intersections[key] === undefined) { yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; } else { if (combinedData[i].y < 0) { intersections[key].accumulatedNegative += combinedData[i].y; } else { intersections[key].accumulatedPositive += combinedData[i].y; } } } for (var xpos in intersections) { if (intersections.hasOwnProperty(xpos)) { yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; } } return { min: yMin, max: yMax }; }; module.exports = Bargraph; /***/ }, /* 57 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var util = __webpack_require__(1); var DOMutil = __webpack_require__(7); var Component = __webpack_require__(21); /** * Legend for Graph2d */ function Legend(body, options, side, linegraphOptions) { this.body = body; this.defaultOptions = { enabled: true, icons: true, iconSize: 20, iconSpacing: 6, left: { visible: true, position: 'top-left' // top/bottom - left,center,right }, right: { visible: true, position: 'top-left' // top/bottom - left,center,right } }; this.side = side; this.options = util.extend({}, this.defaultOptions); this.linegraphOptions = linegraphOptions; this.svgElements = {}; this.dom = {}; this.groups = {}; this.amountOfGroups = 0; this._create(); this.setOptions(options); } Legend.prototype = new Component(); Legend.prototype.clear = function () { this.groups = {}; this.amountOfGroups = 0; }; Legend.prototype.addGroup = function (label, graphOptions) { if (!this.groups.hasOwnProperty(label)) { this.groups[label] = graphOptions; } this.amountOfGroups += 1; }; Legend.prototype.updateGroup = function (label, graphOptions) { this.groups[label] = graphOptions; }; Legend.prototype.removeGroup = function (label) { if (this.groups.hasOwnProperty(label)) { delete this.groups[label]; this.amountOfGroups -= 1; } }; Legend.prototype._create = function () { this.dom.frame = document.createElement('div'); this.dom.frame.className = 'vis-legend'; this.dom.frame.style.position = 'absolute'; this.dom.frame.style.top = '10px'; this.dom.frame.style.display = 'block'; this.dom.textArea = document.createElement('div'); this.dom.textArea.className = 'vis-legend-text'; this.dom.textArea.style.position = 'relative'; this.dom.textArea.style.top = '0px'; this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svg.style.position = 'absolute'; this.svg.style.top = 0 + 'px'; this.svg.style.width = this.options.iconSize + 5 + 'px'; this.svg.style.height = '100%'; this.dom.frame.appendChild(this.svg); this.dom.frame.appendChild(this.dom.textArea); }; /** * Hide the component from the DOM */ Legend.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); } }; /** * Show the component in the DOM (when not already visible). * @return {Boolean} changed */ Legend.prototype.show = function () { // show frame containing the items if (!this.dom.frame.parentNode) { this.body.dom.center.appendChild(this.dom.frame); } }; Legend.prototype.setOptions = function (options) { var fields = ['enabled', 'orientation', 'icons', 'left', 'right']; util.selectiveDeepExtend(fields, this.options, options); }; Legend.prototype.redraw = function () { var activeGroups = 0; var groupArray = Object.keys(this.groups); groupArray.sort(function (a, b) { return a < b ? -1 : 1; }); for (var i = 0; i < groupArray.length; i++) { var groupId = groupArray[i]; if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { activeGroups++; } } if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) { this.hide(); } else { this.show(); if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') { this.dom.frame.style.left = '4px'; this.dom.frame.style.textAlign = 'left'; this.dom.textArea.style.textAlign = 'left'; this.dom.textArea.style.left = this.options.iconSize + 15 + 'px'; this.dom.textArea.style.right = ''; this.svg.style.left = 0 + 'px'; this.svg.style.right = ''; } else { this.dom.frame.style.right = '4px'; this.dom.frame.style.textAlign = 'right'; this.dom.textArea.style.textAlign = 'right'; this.dom.textArea.style.right = this.options.iconSize + 15 + 'px'; this.dom.textArea.style.left = ''; this.svg.style.right = 0 + 'px'; this.svg.style.left = ''; } if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') { this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace('px', '')) + 'px'; this.dom.frame.style.bottom = ''; } else { var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace('px', '')) + 'px'; this.dom.frame.style.top = ''; } if (this.options.icons == false) { this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px'; this.dom.textArea.style.right = ''; this.dom.textArea.style.left = ''; this.svg.style.width = '0px'; } else { this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'; this.drawLegendIcons(); } var content = ''; for (var i = 0; i < groupArray.length; i++) { var groupId = groupArray[i]; if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { content += this.groups[groupId].content + '
'; } } this.dom.textArea.innerHTML = content; this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + 'px'; } }; Legend.prototype.drawLegendIcons = function () { if (this.dom.frame.parentNode) { var groupArray = Object.keys(this.groups); groupArray.sort(function (a, b) { return a < b ? -1 : 1; }); DOMutil.prepareElements(this.svgElements); var padding = window.getComputedStyle(this.dom.frame).paddingTop; var iconOffset = Number(padding.replace('px', '')); var x = iconOffset; var iconWidth = this.options.iconSize; var iconHeight = 0.75 * this.options.iconSize; var y = iconOffset + 0.5 * iconHeight + 3; this.svg.style.width = iconWidth + 5 + iconOffset + 'px'; for (var i = 0; i < groupArray.length; i++) { var groupId = groupArray[i]; if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); y += iconHeight + this.options.iconSpacing; } } DOMutil.cleanupElements(this.svgElements); } }; module.exports = Legend; /***/ }, /* 58 */ /***/ function(module, exports) { /** * This object contains all possible options. It will check if the types are correct, if required if the option is one * of the allowed values. * * __any__ means that the name of the property does not matter. * __type__ is a required field for all objects and contains the allowed types of all objects */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var string = 'string'; var boolean = 'boolean'; var number = 'number'; var array = 'array'; var date = 'date'; var object = 'object'; // should only be in a __type__ property var dom = 'dom'; var moment = 'moment'; var any = 'any'; var allOptions = { configure: { enabled: { boolean: boolean }, filter: { boolean: boolean, 'function': 'function' }, container: { dom: dom }, __type__: { object: object, boolean: boolean, 'function': 'function' } }, //globals : yAxisOrientation: { string: ['left', 'right'] }, defaultGroup: { string: string }, sort: { boolean: boolean }, sampling: { boolean: boolean }, stack: { boolean: boolean }, graphHeight: { string: string, number: number }, shaded: { enabled: { boolean: boolean }, orientation: { string: ['bottom', 'top'] }, // top, bottom __type__: { boolean: boolean, object: object } }, style: { string: ['line', 'bar', 'points'] }, // line, bar barChart: { width: { number: number }, sideBySide: { boolean: boolean }, align: { string: ['left', 'center', 'right'] }, __type__: { object: object } }, interpolation: { enabled: { boolean: boolean }, parametrization: { string: ['centripetal', 'chordal', 'uniform'] }, // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) alpha: { number: number }, __type__: { object: object, boolean: boolean } }, drawPoints: { enabled: { boolean: boolean }, size: { number: number }, style: { string: ['square', 'circle'] }, // square, circle __type__: { object: object, boolean: boolean } }, dataAxis: { showMinorLabels: { boolean: boolean }, showMajorLabels: { boolean: boolean }, icons: { boolean: boolean }, width: { string: string, number: number }, visible: { boolean: boolean }, alignZeros: { boolean: boolean }, left: { range: { min: { number: number }, max: { number: number }, __type__: { object: object } }, format: { 'function': 'function' }, title: { text: { string: string, number: number }, style: { string: string }, __type__: { object: object } }, __type__: { object: object } }, right: { range: { min: { number: number }, max: { number: number }, __type__: { object: object } }, format: { 'function': 'function' }, title: { text: { string: string, number: number }, style: { string: string }, __type__: { object: object } }, __type__: { object: object } }, __type__: { object: object } }, legend: { enabled: { boolean: boolean }, icons: { boolean: boolean }, left: { visible: { boolean: boolean }, position: { string: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] }, __type__: { object: object } }, right: { visible: { boolean: boolean }, position: { string: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] }, __type__: { object: object } }, __type__: { object: object, boolean: boolean } }, groups: { visibility: { any: any }, __type__: { object: object } }, autoResize: { boolean: boolean }, clickToUse: { boolean: boolean }, end: { number: number, date: date, string: string, moment: moment }, format: { minorLabels: { millisecond: { string: string, 'undefined': 'undefined' }, second: { string: string, 'undefined': 'undefined' }, minute: { string: string, 'undefined': 'undefined' }, hour: { string: string, 'undefined': 'undefined' }, weekday: { string: string, 'undefined': 'undefined' }, day: { string: string, 'undefined': 'undefined' }, month: { string: string, 'undefined': 'undefined' }, year: { string: string, 'undefined': 'undefined' }, __type__: { object: object } }, majorLabels: { millisecond: { string: string, 'undefined': 'undefined' }, second: { string: string, 'undefined': 'undefined' }, minute: { string: string, 'undefined': 'undefined' }, hour: { string: string, 'undefined': 'undefined' }, weekday: { string: string, 'undefined': 'undefined' }, day: { string: string, 'undefined': 'undefined' }, month: { string: string, 'undefined': 'undefined' }, year: { string: string, 'undefined': 'undefined' }, __type__: { object: object } }, __type__: { object: object } }, height: { string: string, number: number }, hiddenDates: { object: object, array: array }, locale: { string: string }, locales: { __any__: { any: any }, __type__: { object: object } }, max: { date: date, number: number, string: string, moment: moment }, maxHeight: { number: number, string: string }, min: { date: date, number: number, string: string, moment: moment }, minHeight: { number: number, string: string }, moveable: { boolean: boolean }, multiselect: { boolean: boolean }, orientation: { string: string }, showCurrentTime: { boolean: boolean }, showMajorLabels: { boolean: boolean }, showMinorLabels: { boolean: boolean }, start: { date: date, number: number, string: string, moment: moment }, timeAxis: { scale: { string: string, 'undefined': 'undefined' }, step: { number: number, 'undefined': 'undefined' }, __type__: { object: object } }, width: { string: string, number: number }, zoomable: { boolean: boolean }, zoomMax: { number: number }, zoomMin: { number: number }, __type__: { object: object } }; var configureOptions = { global: { //yAxisOrientation: ['left','right'], // TDOO: enable as soon as Grahp2d doesn't crash when changing this on the fly sort: true, sampling: true, stack: false, shaded: { enabled: false, orientation: ['top', 'bottom'] // top, bottom }, style: ['line', 'bar', 'points'], // line, bar barChart: { width: [50, 5, 100, 5], sideBySide: false, align: ['left', 'center', 'right'] // left, center, right }, interpolation: { enabled: true, parametrization: ['centripetal', 'chordal', 'uniform'] // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) }, drawPoints: { enabled: true, size: [6, 2, 30, 1], style: ['square', 'circle'] // square, circle }, dataAxis: { showMinorLabels: true, showMajorLabels: true, icons: false, width: [40, 0, 200, 1], visible: true, alignZeros: true, left: { //range: {min:'undefined': 'undefined'ined,max:'undefined': 'undefined'ined}, //format: function (value) {return value;}, title: { text: '', style: '' } }, right: { //range: {min:'undefined': 'undefined'ined,max:'undefined': 'undefined'ined}, //format: function (value) {return value;}, title: { text: '', style: '' } } }, legend: { enabled: false, icons: true, left: { visible: true, position: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] // top/bottom - left,right }, right: { visible: true, position: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] // top/bottom - left,right } }, autoResize: true, clickToUse: false, end: '', format: { minorLabels: { millisecond: 'SSS', second: 's', minute: 'HH:mm', hour: 'HH:mm', weekday: 'ddd D', day: 'D', month: 'MMM', year: 'YYYY' }, majorLabels: { millisecond: 'HH:mm:ss', second: 'D MMMM HH:mm', minute: 'ddd D MMMM', hour: 'ddd D MMMM', weekday: 'MMMM YYYY', day: 'MMMM YYYY', month: 'YYYY', year: '' } }, height: '', locale: '', max: '', maxHeight: '', min: '', minHeight: '', moveable: true, orientation: ['both', 'bottom', 'top'], showCurrentTime: false, showMajorLabels: true, showMinorLabels: true, start: '', width: '100%', zoomable: true, zoomMax: [315360000000000, 10, 315360000000000, 1], zoomMin: [10, 10, 315360000000000, 1] } }; exports.allOptions = allOptions; exports.configureOptions = configureOptions; /***/ }, /* 59 */ /***/ function(module, exports, __webpack_require__) { // Load custom shapes into CanvasRenderingContext2D 'use strict'; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _modulesGroups = __webpack_require__(60); var _modulesGroups2 = _interopRequireDefault(_modulesGroups); var _modulesNodesHandler = __webpack_require__(61); var _modulesNodesHandler2 = _interopRequireDefault(_modulesNodesHandler); var _modulesEdgesHandler = __webpack_require__(81); var _modulesEdgesHandler2 = _interopRequireDefault(_modulesEdgesHandler); var _modulesPhysicsEngine = __webpack_require__(88); var _modulesPhysicsEngine2 = _interopRequireDefault(_modulesPhysicsEngine); var _modulesClustering = __webpack_require__(97); var _modulesClustering2 = _interopRequireDefault(_modulesClustering); var _modulesCanvasRenderer = __webpack_require__(99); var _modulesCanvasRenderer2 = _interopRequireDefault(_modulesCanvasRenderer); var _modulesCanvas = __webpack_require__(100); var _modulesCanvas2 = _interopRequireDefault(_modulesCanvas); var _modulesView = __webpack_require__(101); var _modulesView2 = _interopRequireDefault(_modulesView); var _modulesInteractionHandler = __webpack_require__(102); var _modulesInteractionHandler2 = _interopRequireDefault(_modulesInteractionHandler); var _modulesSelectionHandler = __webpack_require__(105); var _modulesSelectionHandler2 = _interopRequireDefault(_modulesSelectionHandler); var _modulesLayoutEngine = __webpack_require__(106); var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); var _modulesManipulationSystem = __webpack_require__(107); var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); var _sharedConfigurator = __webpack_require__(45); var _sharedConfigurator2 = _interopRequireDefault(_sharedConfigurator); var _sharedValidator = __webpack_require__(47); var _sharedValidator2 = _interopRequireDefault(_sharedValidator); var _optionsJs = __webpack_require__(108); __webpack_require__(109); var Emitter = __webpack_require__(13); var Hammer = __webpack_require__(23); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var dotparser = __webpack_require__(110); var gephiParser = __webpack_require__(111); var Images = __webpack_require__(112); var Activator = __webpack_require__(42); var locales = __webpack_require__(113); /** * @constructor Network * Create a network visualization, displaying nodes and edges. * * @param {Element} container The DOM element in which the Network will * be created. Normally a div element. * @param {Object} data An object containing parameters * {Array} nodes * {Array} edges * @param {Object} options Options */ function Network(container, data, options) { var _this = this; if (!(this instanceof Network)) { throw new SyntaxError('Constructor must be called with the new operator'); } // set constant values this.options = {}; this.defaultOptions = { locale: 'en', locales: locales, clickToUse: false }; util.extend(this.options, this.defaultOptions); // containers for nodes and edges this.body = { container: container, nodes: {}, nodeIndices: [], edges: {}, edgeIndices: [], emitter: { on: this.on.bind(this), off: this.off.bind(this), emit: this.emit.bind(this), once: this.once.bind(this) }, eventListeners: { onTap: function onTap() {}, onTouch: function onTouch() {}, onDoubleTap: function onDoubleTap() {}, onHold: function onHold() {}, onDragStart: function onDragStart() {}, onDrag: function onDrag() {}, onDragEnd: function onDragEnd() {}, onMouseWheel: function onMouseWheel() {}, onPinch: function onPinch() {}, onMouseMove: function onMouseMove() {}, onRelease: function onRelease() {}, onContext: function onContext() {} }, data: { nodes: null, // A DataSet or DataView edges: null // A DataSet or DataView }, functions: { createNode: function createNode() {}, createEdge: function createEdge() {}, getPointer: function getPointer() {} }, view: { scale: 1, translation: { x: 0, y: 0 } } }; // bind the event listeners this.bindEventListeners(); // setting up all modules this.images = new Images(function () { return _this.body.emitter.emit('_requestRedraw'); }); // object with images this.groups = new _modulesGroups2['default'](); // object with groups this.canvas = new _modulesCanvas2['default'](this.body); // DOM handler this.selectionHandler = new _modulesSelectionHandler2['default'](this.body, this.canvas); // Selection handler this.interactionHandler = new _modulesInteractionHandler2['default'](this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key this.view = new _modulesView2['default'](this.body, this.canvas); // camera handler, does animations and zooms this.renderer = new _modulesCanvasRenderer2['default'](this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into this.physics = new _modulesPhysicsEngine2['default'](this.body); // physics engine, does all the simulations this.layoutEngine = new _modulesLayoutEngine2['default'](this.body); // layout engine for inital layout and hierarchical layout this.clustering = new _modulesClustering2['default'](this.body); // clustering api this.manipulation = new _modulesManipulationSystem2['default'](this.body, this.canvas, this.selectionHandler); // data manipulation system this.nodesHandler = new _modulesNodesHandler2['default'](this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options this.edgesHandler = new _modulesEdgesHandler2['default'](this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options // create the DOM elements this.canvas._create(); // apply options this.setOptions(options); // load data (the disable start variable will be the same as the enabled clustering) this.setData(data); } // Extend Network with an Emitter mixin Emitter(Network.prototype); /** * Set options * @param {Object} options */ Network.prototype.setOptions = function (options) { var _this2 = this; if (options !== undefined) { var errorFound = _sharedValidator2['default'].validate(options, _optionsJs.allOptions); if (errorFound === true) { console.log('%cErrors have been found in the supplied options object.', _sharedValidator.printStyle); } // copy the global fields over var fields = ['locale', 'locales', 'clickToUse']; util.selectiveDeepExtend(fields, this.options, options); // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. options = this.layoutEngine.setOptions(options.layout, options); this.canvas.setOptions(options); // options for canvas are in globals // pass the options to the modules this.groups.setOptions(options.groups); this.nodesHandler.setOptions(options.nodes); this.edgesHandler.setOptions(options.edges); this.physics.setOptions(options.physics); this.manipulation.setOptions(options.manipulation, options, this.options); // manipulation uses the locales in the globals this.interactionHandler.setOptions(options.interaction); this.renderer.setOptions(options.interaction); // options for rendering are in interaction this.selectionHandler.setOptions(options.interaction); // options for selection are in interaction // reload the settings of the nodes to apply changes in groups that are not referenced by pointer. if (options.groups !== undefined) { this.body.emitter.emit('refreshNodes'); } // these two do not have options at the moment, here for completeness //this.view.setOptions(options.view); //this.clustering.setOptions(options.clustering); if ('configure' in options) { if (!this.configurator) { this.configurator = new _sharedConfigurator2['default'](this, this.body.container, _optionsJs.configureOptions, this.canvas.pixelRatio); } this.configurator.setOptions(options.configure); } // if the configuration system is enabled, copy all options and put them into the config system if (this.configurator && this.configurator.options.enabled === true) { var networkOptions = { nodes: {}, edges: {}, layout: {}, interaction: {}, manipulation: {}, physics: {}, global: {} }; util.deepExtend(networkOptions.nodes, this.nodesHandler.options); util.deepExtend(networkOptions.edges, this.edgesHandler.options); util.deepExtend(networkOptions.layout, this.layoutEngine.options); // load the selectionHandler and render default options in to the interaction group util.deepExtend(networkOptions.interaction, this.selectionHandler.options); util.deepExtend(networkOptions.interaction, this.renderer.options); util.deepExtend(networkOptions.interaction, this.interactionHandler.options); util.deepExtend(networkOptions.manipulation, this.manipulation.options); util.deepExtend(networkOptions.physics, this.physics.options); // load globals into the global object util.deepExtend(networkOptions.global, this.canvas.options); util.deepExtend(networkOptions.global, this.options); this.configurator.setModuleOptions(networkOptions); } // handle network global options if (options.clickToUse !== undefined) { if (options.clickToUse === true) { if (this.activator === undefined) { this.activator = new Activator(this.canvas.frame); this.activator.on('change', function () { _this2.body.emitter.emit('activate'); }); } } else { if (this.activator !== undefined) { this.activator.destroy(); delete this.activator; } this.body.emitter.emit('activate'); } } else { this.body.emitter.emit('activate'); } this.canvas.setSize(); // start the physics simulation. Can be safely called multiple times. this.body.emitter.emit('startSimulation'); } }; /** * Update the this.body.nodeIndices with the most recent node index list * @private */ Network.prototype._updateVisibleIndices = function () { var nodes = this.body.nodes; var edges = this.body.edges; this.body.nodeIndices = []; this.body.edgeIndices = []; for (var nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { if (nodes[nodeId].options.hidden === false) { this.body.nodeIndices.push(nodeId); } } } for (var edgeId in edges) { if (edges.hasOwnProperty(edgeId)) { if (edges[edgeId].options.hidden === false) { this.body.edgeIndices.push(edgeId); } } } }; /** * Bind all events */ Network.prototype.bindEventListeners = function () { var _this3 = this; // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. this.body.emitter.on('_dataChanged', function () { // update shortcut lists _this3._updateVisibleIndices(); _this3.physics.updatePhysicsData(); _this3.body.emitter.emit('_requestRedraw'); // call the dataUpdated event because the only difference between the two is the updating of the indices _this3.body.emitter.emit('_dataUpdated'); }); // this is called when options of EXISTING nodes or edges have changed. this.body.emitter.on('_dataUpdated', function () { // update values _this3._updateValueRange(_this3.body.nodes); _this3._updateValueRange(_this3.body.edges); // start simulation (can be called safely, even if already running) _this3.body.emitter.emit('startSimulation'); _this3.body.emitter.emit('_requestRedraw'); }); }; /** * Set nodes and edges, and optionally options as well. * * @param {Object} data Object containing parameters: * {Array | DataSet | DataView} [nodes] Array with nodes * {Array | DataSet | DataView} [edges] Array with edges * {String} [dot] String containing data in DOT format * {String} [gephi] String containing data in gephi JSON format * {Options} [options] Object with options */ Network.prototype.setData = function (data) { // reset the physics engine. this.body.emitter.emit('resetPhysics'); this.body.emitter.emit('_resetData'); // unselect all to ensure no selections from old data are carried over. this.selectionHandler.unselectAll(); if (data && data.dot && (data.nodes || data.edges)) { throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); } // set options this.setOptions(data && data.options); // set all data if (data && data.dot) { console.log('The dot property has been depricated. Please use the static convertDot method to convert DOT into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertDot(dotString);'); // parse DOT file var dotData = dotparser.DOTToGraph(data.dot); this.setData(dotData); return; } else if (data && data.gephi) { // parse DOT file console.log('The gephi property has been depricated. Please use the static convertGephi method to convert gephi into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertGephi(gephiJson);'); var gephiData = gephiParser.parseGephi(data.gephi); this.setData(gephiData); return; } else { this.nodesHandler.setData(data && data.nodes, true); this.edgesHandler.setData(data && data.edges, true); } // emit change in data this.body.emitter.emit('_dataChanged'); // find a stable position or start animating to a stable position this.body.emitter.emit('initPhysics'); }; /** * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. * var network = new vis.Network(..); * network.destroy(); * network = null; */ Network.prototype.destroy = function () { this.body.emitter.emit('destroy'); // clear events this.body.emitter.off(); this.off(); // delete modules delete this.groups; delete this.canvas; delete this.selectionHandler; delete this.interactionHandler; delete this.view; delete this.renderer; delete this.physics; delete this.layoutEngine; delete this.clustering; delete this.manipulation; delete this.nodesHandler; delete this.edgesHandler; delete this.configurator; delete this.images; for (var nodeId in this.body.nodes) { delete this.body.nodes[nodeId]; } for (var edgeId in this.body.edges) { delete this.body.edges[edgeId]; } // remove the container and everything inside it recursively util.recursiveDOMDelete(this.body.container); }; /** * Update the values of all object in the given array according to the current * value range of the objects in the array. * @param {Object} obj An object containing a set of Edges or Nodes * The objects must have a method getValue() and * setValueRange(min, max). * @private */ Network.prototype._updateValueRange = function (obj) { var id; // determine the range of the objects var valueMin = undefined; var valueMax = undefined; var valueTotal = 0; for (id in obj) { if (obj.hasOwnProperty(id)) { var value = obj[id].getValue(); if (value !== undefined) { valueMin = valueMin === undefined ? value : Math.min(value, valueMin); valueMax = valueMax === undefined ? value : Math.max(value, valueMax); valueTotal += value; } } } // adjust the range of all objects if (valueMin !== undefined && valueMax !== undefined) { for (id in obj) { if (obj.hasOwnProperty(id)) { obj[id].setValueRange(valueMin, valueMax, valueTotal); } } } }; /** * Returns true when the Network is active. * @returns {boolean} */ Network.prototype.isActive = function () { return !this.activator || this.activator.active; }; Network.prototype.setSize = function () { return this.canvas.setSize.apply(this.canvas, arguments); }; Network.prototype.canvasToDOM = function () { return this.canvas.canvasToDOM.apply(this.canvas, arguments); }; Network.prototype.DOMtoCanvas = function () { return this.canvas.DOMtoCanvas(this.canvas, arguments); }; Network.prototype.findNode = function () { return this.clustering.findNode.apply(this.clustering, arguments); }; Network.prototype.isCluster = function () { return this.clustering.isCluster.apply(this.clustering, arguments); }; Network.prototype.openCluster = function () { return this.clustering.openCluster.apply(this.clustering, arguments); }; Network.prototype.cluster = function () { return this.clustering.cluster.apply(this.clustering, arguments); }; Network.prototype.getNodesInCluster = function () { return this.clustering.getNodesInCluster.apply(this.clustering, arguments); }; Network.prototype.clusterByConnection = function () { return this.clustering.clusterByConnection.apply(this.clustering, arguments); }; Network.prototype.clusterByHubsize = function () { return this.clustering.clusterByHubsize.apply(this.clustering, arguments); }; Network.prototype.clusterOutliers = function () { return this.clustering.clusterOutliers.apply(this.clustering, arguments); }; Network.prototype.getSeed = function () { return this.layoutEngine.getSeed.apply(this.layoutEngine, arguments); }; Network.prototype.enableEditMode = function () { return this.manipulation.enableEditMode.apply(this.manipulation, arguments); }; Network.prototype.disableEditMode = function () { return this.manipulation.disableEditMode.apply(this.manipulation, arguments); }; Network.prototype.addNodeMode = function () { return this.manipulation.addNodeMode.apply(this.manipulation, arguments); }; Network.prototype.editNode = function () { return this.manipulation.editNode.apply(this.manipulation, arguments); }; Network.prototype.editNodeMode = function () { console.log('Depricated: Please use editNode instead of editNodeMode.');return this.manipulation.editNode.apply(this.manipulation, arguments); }; Network.prototype.addEdgeMode = function () { return this.manipulation.addEdgeMode.apply(this.manipulation, arguments); }; Network.prototype.editEdgeMode = function () { return this.manipulation.editEdgeMode.apply(this.manipulation, arguments); }; Network.prototype.deleteSelected = function () { return this.manipulation.deleteSelected.apply(this.manipulation, arguments); }; Network.prototype.getPositions = function () { return this.nodesHandler.getPositions.apply(this.nodesHandler, arguments); }; Network.prototype.storePositions = function () { return this.nodesHandler.storePositions.apply(this.nodesHandler, arguments); }; Network.prototype.getBoundingBox = function () { return this.nodesHandler.getBoundingBox.apply(this.nodesHandler, arguments); }; Network.prototype.getConnectedNodes = function (objectId) { if (this.body.nodes[objectId] !== undefined) { return this.nodesHandler.getConnectedNodes.apply(this.nodesHandler, arguments); } else { return this.edgesHandler.getConnectedNodes.apply(this.edgesHandler, arguments); } }; Network.prototype.getConnectedEdges = function () { return this.nodesHandler.getConnectedEdges.apply(this.nodesHandler, arguments); }; Network.prototype.startSimulation = function () { return this.physics.startSimulation.apply(this.physics, arguments); }; Network.prototype.stopSimulation = function () { return this.physics.stopSimulation.apply(this.physics, arguments); }; Network.prototype.stabilize = function () { return this.physics.stabilize.apply(this.physics, arguments); }; Network.prototype.getSelection = function () { return this.selectionHandler.getSelection.apply(this.selectionHandler, arguments); }; Network.prototype.getSelectedNodes = function () { return this.selectionHandler.getSelectedNodes.apply(this.selectionHandler, arguments); }; Network.prototype.getSelectedEdges = function () { return this.selectionHandler.getSelectedEdges.apply(this.selectionHandler, arguments); }; Network.prototype.getNodeAt = function () { var node = this.selectionHandler.getNodeAt.apply(this.selectionHandler, arguments); if (node !== undefined && node.id !== undefined) { return node.id; } return node; }; Network.prototype.getEdgeAt = function () { var edge = this.selectionHandler.getEdgeAt.apply(this.selectionHandler, arguments); if (edge !== undefined && edge.id !== undefined) { return edge.id; } return edge; }; Network.prototype.selectNodes = function () { return this.selectionHandler.selectNodes.apply(this.selectionHandler, arguments); }; Network.prototype.selectEdges = function () { return this.selectionHandler.selectEdges.apply(this.selectionHandler, arguments); }; Network.prototype.unselectAll = function () { return this.selectionHandler.unselectAll.apply(this.selectionHandler, arguments); }; Network.prototype.redraw = function () { return this.renderer.redraw.apply(this.renderer, arguments); }; Network.prototype.getScale = function () { return this.view.getScale.apply(this.view, arguments); }; Network.prototype.getViewPosition = function () { return this.view.getViewPosition.apply(this.view, arguments); }; Network.prototype.fit = function () { return this.view.fit.apply(this.view, arguments); }; Network.prototype.moveTo = function () { return this.view.moveTo.apply(this.view, arguments); }; Network.prototype.focus = function () { return this.view.focus.apply(this.view, arguments); }; Network.prototype.releaseNode = function () { return this.view.releaseNode.apply(this.view, arguments); }; module.exports = Network; /***/ }, /* 60 */ /***/ function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var util = __webpack_require__(1); /** * @class Groups * This class can store groups and options specific for groups. */ var Groups = (function () { function Groups() { _classCallCheck(this, Groups); this.clear(); this.defaultIndex = 0; this.groupsArray = []; this.groupIndex = 0; this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } } // 20:bright red ]; this.options = {}; this.defaultOptions = { useDefaultGroups: true }; util.extend(this.options, this.defaultOptions); } _createClass(Groups, [{ key: "setOptions", value: function setOptions(options) { var optionFields = ["useDefaultGroups"]; if (options !== undefined) { for (var groupName in options) { if (options.hasOwnProperty(groupName)) { if (optionFields.indexOf(groupName) === -1) { var group = options[groupName]; this.add(groupName, group); } } } } } }, { key: "clear", /** * Clear all groups */ value: function clear() { this.groups = {}; this.groupsArray = []; } }, { key: "get", /** * get group options of a groupname. If groupname is not found, a new group * is added. * @param {*} groupname Can be a number, string, Date, etc. * @return {Object} group The created group, containing all group options */ value: function get(groupname) { var group = this.groups[groupname]; if (group === undefined) { if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { // create new group var index = this.groupIndex % this.groupsArray.length; this.groupIndex++; group = {}; group.color = this.groups[this.groupsArray[index]]; this.groups[groupname] = group; } else { // create new group var index = this.defaultIndex % this.defaultGroups.length; this.defaultIndex++; group = {}; group.color = this.defaultGroups[index]; this.groups[groupname] = group; } } return group; } }, { key: "add", /** * Add a custom group style * @param {String} groupName * @param {Object} style An object containing borderColor, * backgroundColor, etc. * @return {Object} group The created group object */ value: function add(groupName, style) { this.groups[groupName] = style; this.groupsArray.push(groupName); return style; } }]); return Groups; })(); exports["default"] = Groups; module.exports = exports["default"]; /***/ }, /* 61 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _componentsNode = __webpack_require__(62); var _componentsNode2 = _interopRequireDefault(_componentsNode); var _componentsSharedLabel = __webpack_require__(63); var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var NodesHandler = (function () { function NodesHandler(body, images, groups, layoutEngine) { var _this = this; _classCallCheck(this, NodesHandler); this.body = body; this.images = images; this.groups = groups; this.layoutEngine = layoutEngine; // create the node API in the body container this.body.functions.createNode = this.create.bind(this); this.nodesListeners = { add: function add(event, params) { _this.add(params.items); }, update: function update(event, params) { _this.update(params.items, params.data); }, remove: function remove(event, params) { _this.remove(params.items); } }; this.options = {}; this.defaultOptions = { borderWidth: 1, borderWidthSelected: 2, brokenImage: undefined, color: { border: '#2B7CE9', background: '#97C2FC', highlight: { border: '#2B7CE9', background: '#D2E5FF' }, hover: { border: '#2B7CE9', background: '#D2E5FF' } }, fixed: { x: false, y: false }, font: { color: '#343434', size: 14, // px face: 'arial', background: 'none', strokeWidth: 0, // px strokeColor: '#ffffff', align: 'horizontal' }, group: undefined, hidden: false, icon: { face: 'FontAwesome', //'FontAwesome', code: undefined, //'\uf007', size: 50, //50, color: '#2B7CE9' //'#aa00ff' }, image: undefined, // --> URL label: undefined, level: undefined, mass: 1, physics: true, scaling: { min: 10, max: 30, label: { enabled: false, min: 14, max: 30, maxVisible: 30, drawThreshold: 5 }, customScalingFunction: function customScalingFunction(min, max, total, value) { if (max === min) { return 0.5; } else { var scale = 1 / (max - min); return Math.max(0, (value - min) * scale); } } }, shadow: { enabled: false, size: 10, x: 5, y: 5 }, shape: 'ellipse', size: 25, title: undefined, value: undefined, x: undefined, y: undefined }; util.extend(this.options, this.defaultOptions); this.bindEventListeners(); } _createClass(NodesHandler, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this2 = this; // refresh the nodes. Used when reverting from hierarchical layout this.body.emitter.on('refreshNodes', this.refresh.bind(this)); this.body.emitter.on('refresh', this.refresh.bind(this)); this.body.emitter.on('destroy', function () { delete _this2.body.functions.createNode; delete _this2.nodesListeners.add; delete _this2.nodesListeners.update; delete _this2.nodesListeners.remove; delete _this2.nodesListeners; }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { _componentsNode2['default'].parseOptions(this.options, options); // update the shape in all nodes if (options.shape !== undefined) { for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { this.body.nodes[nodeId].updateShape(); } } } // update the shape size in all nodes if (options.font !== undefined) { _componentsSharedLabel2['default'].parseOptions(this.options.font, options); for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { this.body.nodes[nodeId].updateLabelModule(); this.body.nodes[nodeId]._reset(); } } } // update the shape size in all nodes if (options.size !== undefined) { for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { this.body.nodes[nodeId]._reset(); } } } // update the state of the letiables if needed if (options.hidden !== undefined || options.physics !== undefined) { this.body.emitter.emit('_dataChanged'); } } } }, { key: 'setData', /** * Set a data set with nodes for the network * @param {Array | DataSet | DataView} nodes The data containing the nodes. * @private */ value: function setData(nodes) { var _this3 = this; var doNotEmit = arguments[1] === undefined ? false : arguments[1]; var oldNodesData = this.body.data.nodes; if (nodes instanceof DataSet || nodes instanceof DataView) { this.body.data.nodes = nodes; } else if (Array.isArray(nodes)) { this.body.data.nodes = new DataSet(); this.body.data.nodes.add(nodes); } else if (!nodes) { this.body.data.nodes = new DataSet(); } else { throw new TypeError('Array or DataSet expected'); } if (oldNodesData) { // unsubscribe from old dataset util.forEach(this.nodesListeners, function (callback, event) { oldNodesData.off(event, callback); }); } // remove drawn nodes this.body.nodes = {}; if (this.body.data.nodes) { (function () { // subscribe to new dataset var me = _this3; util.forEach(_this3.nodesListeners, function (callback, event) { me.body.data.nodes.on(event, callback); }); // draw all new nodes var ids = _this3.body.data.nodes.getIds(); _this3.add(ids, true); })(); } if (doNotEmit === false) { this.body.emitter.emit('_dataChanged'); } } }, { key: 'add', /** * Add nodes * @param {Number[] | String[]} ids * @private */ value: function add(ids) { var doNotEmit = arguments[1] === undefined ? false : arguments[1]; var id = undefined; var newNodes = []; for (var i = 0; i < ids.length; i++) { id = ids[i]; var properties = this.body.data.nodes.get(id); var node = this.create(properties); newNodes.push(node); this.body.nodes[id] = node; // note: this may replace an existing node } this.layoutEngine.positionInitially(newNodes); if (doNotEmit === false) { this.body.emitter.emit('_dataChanged'); } } }, { key: 'update', /** * Update existing nodes, or create them when not yet existing * @param {Number[] | String[]} ids * @private */ value: function update(ids, changedData) { var nodes = this.body.nodes; var dataChanged = false; for (var i = 0; i < ids.length; i++) { var id = ids[i]; var node = nodes[id]; var data = changedData[i]; if (node !== undefined) { // update node dataChanged = node.setOptions(data); } else { dataChanged = true; // create node node = this.create(data); nodes[id] = node; } } if (dataChanged === true) { this.body.emitter.emit('_dataChanged'); } else { this.body.emitter.emit('_dataUpdated'); } } }, { key: 'remove', /** * Remove existing nodes. If nodes do not exist, the method will just ignore it. * @param {Number[] | String[]} ids * @private */ value: function remove(ids) { var nodes = this.body.nodes; for (var i = 0; i < ids.length; i++) { var id = ids[i]; delete nodes[id]; } this.body.emitter.emit('_dataChanged'); } }, { key: 'create', /** * create a node * @param properties * @param constructorClass */ value: function create(properties) { var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; return new constructorClass(properties, this.body, this.images, this.groups, this.options); } }, { key: 'refresh', value: function refresh() { var nodes = this.body.nodes; for (var nodeId in nodes) { var node = undefined; if (nodes.hasOwnProperty(nodeId)) { node = nodes[nodeId]; } var data = this.body.data.nodes._data[nodeId]; if (node !== undefined && data !== undefined) { node.setOptions({ fixed: false }); node.setOptions(data); } } } }, { key: 'getPositions', /** * Returns the positions of the nodes. * @param ids --> optional, can be array of nodeIds, can be string * @returns {{}} */ value: function getPositions(ids) { var dataArray = {}; if (ids !== undefined) { if (Array.isArray(ids) === true) { for (var i = 0; i < ids.length; i++) { if (this.body.nodes[ids[i]] !== undefined) { var node = this.body.nodes[ids[i]]; dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; } } } else { if (this.body.nodes[ids] !== undefined) { var node = this.body.nodes[ids]; dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; } } } else { for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { var node = this.body.nodes[nodeId]; dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; } } } return dataArray; } }, { key: 'storePositions', /** * Load the XY positions of the nodes into the dataset. */ value: function storePositions() { // todo: add support for clusters and hierarchical. var dataArray = []; var dataset = this.body.data.nodes.getDataSet(); for (var nodeId in dataset._data) { if (dataset._data.hasOwnProperty(nodeId)) { var node = this.body.nodes[nodeId]; if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) { dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); } } } dataset.update(dataArray); } }, { key: 'getBoundingBox', /** * get the bounding box of a node. * @param nodeId * @returns {j|*} */ value: function getBoundingBox(nodeId) { if (this.body.nodes[nodeId] !== undefined) { return this.body.nodes[nodeId].shape.boundingBox; } } }, { key: 'getConnectedNodes', /** * Get the Ids of nodes connected to this node. * @param nodeId * @returns {Array} */ value: function getConnectedNodes(nodeId) { var nodeList = []; if (this.body.nodes[nodeId] !== undefined) { var node = this.body.nodes[nodeId]; var nodeObj = {}; // used to quickly check if node already exists for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; if (edge.toId == nodeId) { // these are double equals since ids can be numeric or string if (nodeObj[edge.fromId] === undefined) { nodeList.push(edge.fromId); nodeObj[edge.fromId] = true; } } else if (edge.fromId == nodeId) { // these are double equals since ids can be numeric or string if (nodeObj[edge.toId] === undefined) { nodeList.push(edge.toId); nodeObj[edge.toId] = true; } } } } return nodeList; } }, { key: 'getConnectedEdges', /** * Get the ids of the edges connected to this node. * @param nodeId * @returns {*} */ value: function getConnectedEdges(nodeId) { var edgeList = []; if (this.body.nodes[nodeId] !== undefined) { var node = this.body.nodes[nodeId]; for (var i = 0; i < node.edges.length; i++) { edgeList.push(node.edges[i].id); } } else { console.log('NodeId provided for getConnectedEdges does not exist. Provided: ', nodeId); } return edgeList; } }]); return NodesHandler; })(); exports['default'] = NodesHandler; module.exports = exports['default']; /***/ }, /* 62 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _sharedLabel = __webpack_require__(63); var _sharedLabel2 = _interopRequireDefault(_sharedLabel); var _nodesShapesBox = __webpack_require__(64); var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); var _nodesShapesCircle = __webpack_require__(66); var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); var _nodesShapesCircularImage = __webpack_require__(68); var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); var _nodesShapesDatabase = __webpack_require__(69); var _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); var _nodesShapesDiamond = __webpack_require__(70); var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); var _nodesShapesDot = __webpack_require__(72); var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); var _nodesShapesEllipse = __webpack_require__(73); var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); var _nodesShapesIcon = __webpack_require__(74); var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); var _nodesShapesImage = __webpack_require__(75); var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); var _nodesShapesSquare = __webpack_require__(76); var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); var _nodesShapesStar = __webpack_require__(77); var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); var _nodesShapesText = __webpack_require__(78); var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); var _nodesShapesTriangle = __webpack_require__(79); var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); var _nodesShapesTriangleDown = __webpack_require__(80); var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); var _sharedValidator = __webpack_require__(47); var _sharedValidator2 = _interopRequireDefault(_sharedValidator); var util = __webpack_require__(1); /** * @class Node * A node. A node can be connected to other nodes via one or multiple edges. * @param {object} options An object containing options for the node. All * options are optional, except for the id. * {number} id Id of the node. Required * {string} label Text label for the node * {number} x Horizontal position of the node * {number} y Vertical position of the node * {string} shape Node shape, available: * "database", "circle", "ellipse", * "box", "image", "text", "dot", * "star", "triangle", "triangleDown", * "square", "icon" * {string} image An image url * {string} title An title text, can be HTML * {anytype} group A group name or number * @param {Network.Images} imagelist A list with images. Only needed * when the node has an image * @param {Network.Groups} grouplist A list with groups. Needed for * retrieving group options * @param {Object} constants An object with default values for * example for the color * */ var Node = (function () { function Node(options, body, imagelist, grouplist, globalOptions) { _classCallCheck(this, Node); this.options = util.bridgeObject(globalOptions); this.body = body; this.edges = []; // all edges connected to this node // set defaults for the options this.id = undefined; this.imagelist = imagelist; this.grouplist = grouplist; // state options this.x = undefined; this.y = undefined; this.baseSize = this.options.size; this.baseFontSize = this.options.font.size; this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate this.selected = false; this.hover = false; this.labelModule = new _sharedLabel2['default'](this.body, this.options); this.setOptions(options); } _createClass(Node, [{ key: 'attachEdge', /** * Attach a edge to the node * @param {Edge} edge */ value: function attachEdge(edge) { if (this.edges.indexOf(edge) === -1) { this.edges.push(edge); } } }, { key: 'detachEdge', /** * Detach a edge from the node * @param {Edge} edge */ value: function detachEdge(edge) { var index = this.edges.indexOf(edge); if (index != -1) { this.edges.splice(index, 1); } } }, { key: 'togglePhysics', /** * Enable or disable the physics. * @param status */ value: function togglePhysics(status) { this.options.physics = status; } }, { key: 'setOptions', /** * Set or overwrite options for the node * @param {Object} options an object with options * @param {Object} constants and object with default, global options */ value: function setOptions(options) { if (!options) { return; } // basic options if (options.id !== undefined) { this.id = options.id; } if (this.id === undefined) { throw 'Node must have an id'; } // set these options locally if (options.x !== undefined) { this.x = parseInt(options.x);this.predefinedPosition = true; } if (options.y !== undefined) { this.y = parseInt(options.y);this.predefinedPosition = true; } if (options.size !== undefined) { this.baseSize = options.size; } if (options.value !== undefined) { options.value = parseFloat(options.value); } // copy group options if (typeof options.group === 'number' || typeof options.group === 'string' && options.group != '') { var groupObj = this.grouplist.get(options.group); util.deepExtend(this.options, groupObj); // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. this.options.color = util.parseColor(this.options.color); } // this transforms all shorthands into fully defined options Node.parseOptions(this.options, options, true); // load the images if (this.options.image !== undefined && this.options.image != '') { if (this.imagelist) { this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); } else { throw 'No imagelist provided'; } } this.updateShape(); this.updateLabelModule(); // reset the size of the node, this can be changed this._reset(); if (options.hidden !== undefined || options.physics !== undefined) { return true; } return false; } }, { key: 'updateLabelModule', value: function updateLabelModule() { if (this.options.label === undefined || this.options.label === null) { this.options.label = ''; } this.labelModule.setOptions(this.options, true); if (this.labelModule.baseSize !== undefined) { this.baseFontSize = this.labelModule.baseSize; } } }, { key: 'updateShape', value: function updateShape() { // choose draw method depending on the shape switch (this.options.shape) { case 'box': this.shape = new _nodesShapesBox2['default'](this.options, this.body, this.labelModule); break; case 'circle': this.shape = new _nodesShapesCircle2['default'](this.options, this.body, this.labelModule); break; case 'circularImage': this.shape = new _nodesShapesCircularImage2['default'](this.options, this.body, this.labelModule, this.imageObj); break; case 'database': this.shape = new _nodesShapesDatabase2['default'](this.options, this.body, this.labelModule); break; case 'diamond': this.shape = new _nodesShapesDiamond2['default'](this.options, this.body, this.labelModule); break; case 'dot': this.shape = new _nodesShapesDot2['default'](this.options, this.body, this.labelModule); break; case 'ellipse': this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); break; case 'icon': this.shape = new _nodesShapesIcon2['default'](this.options, this.body, this.labelModule); break; case 'image': this.shape = new _nodesShapesImage2['default'](this.options, this.body, this.labelModule, this.imageObj); break; case 'square': this.shape = new _nodesShapesSquare2['default'](this.options, this.body, this.labelModule); break; case 'star': this.shape = new _nodesShapesStar2['default'](this.options, this.body, this.labelModule); break; case 'text': this.shape = new _nodesShapesText2['default'](this.options, this.body, this.labelModule); break; case 'triangle': this.shape = new _nodesShapesTriangle2['default'](this.options, this.body, this.labelModule); break; case 'triangleDown': this.shape = new _nodesShapesTriangleDown2['default'](this.options, this.body, this.labelModule); break; default: this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); break; } this._reset(); } }, { key: 'select', /** * select this node */ value: function select() { this.selected = true; this._reset(); } }, { key: 'unselect', /** * unselect this node */ value: function unselect() { this.selected = false; this._reset(); } }, { key: '_reset', /** * Reset the calculated size of the node, forces it to recalculate its size * @private */ value: function _reset() { this.shape.width = undefined; this.shape.height = undefined; } }, { key: 'getTitle', /** * get the title of this node. * @return {string} title The title of the node, or undefined when no title * has been set. */ value: function getTitle() { return this.options.title; } }, { key: 'distanceToBorder', /** * Calculate the distance to the border of the Node * @param {CanvasRenderingContext2D} ctx * @param {Number} angle Angle in radians * @returns {number} distance Distance to the border in pixels */ value: function distanceToBorder(ctx, angle) { return this.shape.distanceToBorder(ctx, angle); } }, { key: 'isFixed', /** * Check if this node has a fixed x and y position * @return {boolean} true if fixed, false if not */ value: function isFixed() { return this.options.fixed.x && this.options.fixed.y; } }, { key: 'isSelected', /** * check if this node is selecte * @return {boolean} selected True if node is selected, else false */ value: function isSelected() { return this.selected; } }, { key: 'getValue', /** * Retrieve the value of the node. Can be undefined * @return {Number} value */ value: function getValue() { return this.options.value; } }, { key: 'setValueRange', /** * Adjust the value range of the node. The node will adjust it's size * based on its value. * @param {Number} min * @param {Number} max */ value: function setValueRange(min, max, total) { if (this.options.value !== undefined) { var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); var sizeDiff = this.options.scaling.max - this.options.scaling.min; if (this.options.scaling.label.enabled === true) { var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; this.options.font.size = this.options.scaling.label.min + scale * fontDiff; } this.options.size = this.options.scaling.min + scale * sizeDiff; } else { this.options.size = this.baseSize; this.options.font.size = this.baseFontSize; } } }, { key: 'draw', /** * Draw this node in the given canvas * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); * @param {CanvasRenderingContext2D} ctx */ value: function draw(ctx) { this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); } }, { key: 'updateBoundingBox', /** * Update the bounding box of the shape */ value: function updateBoundingBox() { this.shape.updateBoundingBox(this.x, this.y); } }, { key: 'resize', /** * Recalculate the size of this node in the given canvas * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); * @param {CanvasRenderingContext2D} ctx */ value: function resize(ctx) { this.shape.resize(ctx); } }, { key: 'isOverlappingWith', /** * Check if this object is overlapping with the provided object * @param {Object} obj an object with parameters left, top, right, bottom * @return {boolean} True if location is located on node */ value: function isOverlappingWith(obj) { return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; } }, { key: 'isBoundingBoxOverlappingWith', /** * Check if this object is overlapping with the provided object * @param {Object} obj an object with parameters left, top, right, bottom * @return {boolean} True if location is located on node */ value: function isBoundingBoxOverlappingWith(obj) { return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; } }], [{ key: 'parseOptions', /** * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. * Static so it can also be used by the handler. * @param parentOptions * @param newOptions */ value: function parseOptions(parentOptions, newOptions) { var allowDeletion = arguments[2] === undefined ? false : arguments[2]; var fields = ['color', 'font', 'fixed', 'shadow']; util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); // merge the shadow options into the parent. util.mergeOptions(parentOptions, newOptions, 'shadow'); // individual shape newOptions if (newOptions.color !== undefined && newOptions.color !== null) { var parsedColor = util.parseColor(newOptions.color); util.fillIfDefined(parentOptions.color, parsedColor); } else if (allowDeletion === true && newOptions.color === null) { parentOptions.color = undefined; delete parentOptions.color; } // handle the fixed options if (newOptions.fixed !== undefined && newOptions.fixed !== null) { if (typeof newOptions.fixed === 'boolean') { parentOptions.fixed.x = newOptions.fixed; parentOptions.fixed.y = newOptions.fixed; } else { if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { parentOptions.fixed.x = newOptions.fixed.x; } if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { parentOptions.fixed.y = newOptions.fixed.y; } } } // handle the font options if (newOptions.font !== undefined) { _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); } // handle the scaling options, specifically the label part if (newOptions.scaling !== undefined) { util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); } } }]); return Node; })(); exports['default'] = Node; module.exports = exports['default']; /***/ }, /* 63 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); var Label = (function () { function Label(body, options) { _classCallCheck(this, Label); this.body = body; this.pointToSelf = false; this.baseSize = undefined; this.setOptions(options); this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached } _createClass(Label, [{ key: 'setOptions', value: function setOptions(options) { var allowDeletion = arguments[1] === undefined ? false : arguments[1]; this.options = options; if (options.label !== undefined) { this.labelDirty = true; } if (options.font !== undefined) { Label.parseOptions(this.options.font, options, allowDeletion); if (typeof options.font === 'string') { this.baseSize = this.options.font.size; } else if (typeof options.font === 'object') { if (options.font.size !== undefined) { this.baseSize = options.font.size; } } } } }, { key: 'draw', /** * Main function. This is called from anything that wants to draw a label. * @param ctx * @param x * @param y * @param selected * @param baseline */ value: function draw(ctx, x, y, selected) { var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; // if no label, return if (this.options.label === undefined) return; // check if we have to render the label var viewFontSize = this.options.font.size * this.body.view.scale; if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) return; // update the size cache if required this.calculateLabelSize(ctx, selected, x, y, baseline); // create the fontfill background this._drawBackground(ctx); // draw text this._drawText(ctx, selected, x, y, baseline); } }, { key: '_drawBackground', /** * Draws the label background * @param {CanvasRenderingContext2D} ctx * @private */ value: function _drawBackground(ctx) { if (this.options.font.background !== undefined && this.options.font.background !== 'none') { ctx.fillStyle = this.options.font.background; var lineMargin = 2; switch (this.options.font.align) { case 'middle': ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); break; case 'top': ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); break; case 'bottom': ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); break; default: ctx.fillRect(this.size.left, this.size.top - 0.5 * lineMargin, this.size.width, this.size.height); break; } } } }, { key: '_drawText', /** * * @param ctx * @param x * @param baseline * @private */ value: function _drawText(ctx, selected, x, y) { var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; var fontSize = this.options.font.size; var viewFontSize = fontSize * this.body.view.scale; // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) if (viewFontSize >= this.options.scaling.label.maxVisible) { fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; } var yLine = this.size.yLine; var _getColor2 = this._getColor(viewFontSize); var _getColor22 = _slicedToArray(_getColor2, 2); var fontColor = _getColor22[0]; var strokeColor = _getColor22[1]; var _setAlignment2 = this._setAlignment(ctx, x, yLine, baseline); var _setAlignment22 = _slicedToArray(_setAlignment2, 2); x = _setAlignment22[0]; yLine = _setAlignment22[1]; // configure context for drawing the text ctx.font = (selected ? 'bold ' : '') + fontSize + 'px ' + this.options.font.face; ctx.fillStyle = fontColor; ctx.textAlign = 'center'; // set the strokeWidth if (this.options.font.strokeWidth > 0) { ctx.lineWidth = this.options.font.strokeWidth; ctx.strokeStyle = strokeColor; ctx.lineJoin = 'round'; } // draw the text for (var i = 0; i < this.lineCount; i++) { if (this.options.font.strokeWidth > 0) { ctx.strokeText(this.lines[i], x, yLine); } ctx.fillText(this.lines[i], x, yLine); yLine += fontSize; } } }, { key: '_setAlignment', value: function _setAlignment(ctx, x, yLine, baseline) { // check for label alignment (for edges) // TODO: make alignment for nodes if (this.options.font.align !== 'horizontal' && this.pointToSelf === false) { x = 0; yLine = 0; var lineMargin = 2; if (this.options.font.align === 'top') { ctx.textBaseline = 'alphabetic'; yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers } else if (this.options.font.align === 'bottom') { ctx.textBaseline = 'hanging'; yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers } else { ctx.textBaseline = 'middle'; } } else { ctx.textBaseline = baseline; } return [x, yLine]; } }, { key: '_getColor', /** * fade in when relative scale is between threshold and threshold - 1. * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. * * @param viewFontSize * @returns {*[]} * @private */ value: function _getColor(viewFontSize) { var fontColor = this.options.font.color || '#000000'; var strokeColor = this.options.font.strokeColor || '#ffffff'; if (viewFontSize <= this.options.scaling.label.drawThreshold) { var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); fontColor = util.overrideOpacity(fontColor, opacity); strokeColor = util.overrideOpacity(strokeColor, opacity); } return [fontColor, strokeColor]; } }, { key: 'getTextSize', /** * * @param ctx * @param selected * @returns {{width: number, height: number}} */ value: function getTextSize(ctx) { var selected = arguments[1] === undefined ? false : arguments[1]; var size = { width: this._processLabel(ctx, selected), height: this.options.font.size * this.lineCount, lineCount: this.lineCount }; return size; } }, { key: 'calculateLabelSize', /** * * @param ctx * @param selected * @param x * @param y * @param baseline */ value: function calculateLabelSize(ctx, selected) { var x = arguments[2] === undefined ? 0 : arguments[2]; var y = arguments[3] === undefined ? 0 : arguments[3]; var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; if (this.labelDirty === true) { this.size.width = this._processLabel(ctx, selected); } this.size.height = this.options.font.size * this.lineCount; this.size.left = x - this.size.width * 0.5; this.size.top = y - this.size.height * 0.5; this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; if (baseline === 'hanging') { this.size.top += 0.5 * this.options.font.size; this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers this.size.yLine += 4; // distance from node } this.labelDirty = false; } }, { key: '_processLabel', /** * This calculates the width as well as explodes the label string and calculates the amount of lines. * @param ctx * @param selected * @returns {number} * @private */ value: function _processLabel(ctx, selected) { var width = 0; var lines = ['']; var lineCount = 0; if (this.options.label !== undefined) { lines = String(this.options.label).split('\n'); lineCount = lines.length; ctx.font = (selected ? 'bold ' : '') + this.options.font.size + 'px ' + this.options.font.face; width = ctx.measureText(lines[0]).width; for (var i = 1; i < lineCount; i++) { var lineWidth = ctx.measureText(lines[i]).width; width = lineWidth > width ? lineWidth : width; } } this.lines = lines; this.lineCount = lineCount; return width; } }], [{ key: 'parseOptions', value: function parseOptions(parentOptions, newOptions) { var allowDeletion = arguments[2] === undefined ? false : arguments[2]; if (typeof newOptions.font === 'string') { var newOptionsArray = newOptions.font.split(' '); parentOptions.size = newOptionsArray[0].replace('px', ''); parentOptions.face = newOptionsArray[1]; parentOptions.color = newOptionsArray[2]; } else if (typeof newOptions.font === 'object') { util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); } parentOptions.size = Number(parentOptions.size); } }]); return Label; })(); exports['default'] = Label; module.exports = exports['default']; /***/ }, /* 64 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var Box = (function (_NodeBase) { function Box(options, body, labelModule) { _classCallCheck(this, Box); _get(Object.getPrototypeOf(Box.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Box, _NodeBase); _createClass(Box, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { var margin = 5; var textSize = this.labelModule.getTextSize(ctx, selected); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; this.radius = 0.5 * this.width; } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx, selected); this.left = x - this.width / 2; this.top = y - this.height / 2; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; ctx.lineWidth = selected ? selectionLineWidth : borderWidth; ctx.lineWidth /= this.body.view.scale; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; var borderRadius = 6; ctx.roundRect(this.left, this.top, this.width, this.height, borderRadius); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); this.updateBoundingBox(x, y); this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.left = x - this.width * 0.5; this.top = y - this.height * 0.5; this.boundingBox.left = this.left; this.boundingBox.top = this.top; this.boundingBox.bottom = this.top + this.height; this.boundingBox.right = this.left + this.width; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); var a = this.width / 2; var b = this.height / 2; var w = Math.sin(angle) * a; var h = Math.cos(angle) * b; return a * b / Math.sqrt(w * w + h * h); } }]); return Box; })(_utilNodeBase2['default']); exports['default'] = Box; module.exports = exports['default']; /***/ }, /* 65 */ /***/ function(module, exports) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var NodeBase = (function () { function NodeBase(options, body, labelModule) { _classCallCheck(this, NodeBase); this.body = body; this.labelModule = labelModule; this.setOptions(options); this.top = undefined; this.left = undefined; this.height = undefined; this.width = undefined; this.radius = undefined; this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; } _createClass(NodeBase, [{ key: 'setOptions', value: function setOptions(options) { this.options = options; } }, { key: '_distanceToBorder', value: function _distanceToBorder(angle) { var borderWidth = 1; return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; } }, { key: 'enableShadow', value: function enableShadow(ctx) { if (this.options.shadow.enabled === true) { ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = this.options.shadow.size; ctx.shadowOffsetX = this.options.shadow.x; ctx.shadowOffsetY = this.options.shadow.y; } } }, { key: 'disableShadow', value: function disableShadow(ctx) { if (this.options.shadow.enabled === true) { ctx.shadowColor = 'rgba(0,0,0,0)'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } } }]); return NodeBase; })(); exports['default'] = NodeBase; module.exports = exports['default']; /***/ }, /* 66 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilCircleImageBase = __webpack_require__(67); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); var Circle = (function (_CircleImageBase) { function Circle(options, body, labelModule) { _classCallCheck(this, Circle); _get(Object.getPrototypeOf(Circle.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Circle, _CircleImageBase); _createClass(Circle, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { var margin = 5; var textSize = this.labelModule.getTextSize(ctx, selected); var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; this.options.size = diameter / 2; this.width = diameter; this.height = diameter; this.radius = 0.5 * this.width; } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx, selected); this.left = x - this.width / 2; this.top = y - this.height / 2; this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); this.boundingBox.top = y - this.options.size; this.boundingBox.left = x - this.options.size; this.boundingBox.right = x + this.options.size; this.boundingBox.bottom = y + this.options.size; this.updateBoundingBox(x, y); this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.boundingBox.top = y - this.options.size; this.boundingBox.left = x - this.options.size; this.boundingBox.right = x + this.options.size; this.boundingBox.bottom = y + this.options.size; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); var a = this.width / 2; var b = this.height / 2; var w = Math.sin(angle) * a; var h = Math.cos(angle) * b; return a * b / Math.sqrt(w * w + h * h); } }]); return Circle; })(_utilCircleImageBase2['default']); exports['default'] = Circle; module.exports = exports['default']; /***/ }, /* 67 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var CircleImageBase = (function (_NodeBase) { function CircleImageBase(options, body, labelModule) { _classCallCheck(this, CircleImageBase); _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); this.labelOffset = 0; this.imageLoaded = false; } _inherits(CircleImageBase, _NodeBase); _createClass(CircleImageBase, [{ key: '_resizeImage', /** * This function resizes the image by the options size when the image has not yet loaded. If the image has loaded, we * force the update of the size again. * * @private */ value: function _resizeImage() { var force = false; if (!this.imageObj.width || !this.imageObj.height) { // undefined or 0 this.imageLoaded = false; } else if (this.imageLoaded === false) { this.imageLoaded = true; force = true; } if (!this.width || !this.height || force === true) { // undefined or 0 var width, height, ratio; if (this.imageObj.width && this.imageObj.height) { // not undefined or 0 width = 0; height = 0; } if (this.imageObj.width > this.imageObj.height) { ratio = this.imageObj.width / this.imageObj.height; width = this.options.size * 2 * ratio || this.imageObj.width; height = this.options.size * 2 || this.imageObj.height; } else { if (this.imageObj.width && this.imageObj.height) { // not undefined or 0 ratio = this.imageObj.height / this.imageObj.width; } else { ratio = 1; } width = this.options.size * 2 || this.imageObj.width; height = this.options.size * 2 * ratio || this.imageObj.height; } this.width = width; this.height = height; this.radius = 0.5 * this.width; } } }, { key: '_drawRawCircle', value: function _drawRawCircle(ctx, x, y, selected, hover, size) { var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; ctx.lineWidth = selected ? selectionLineWidth : borderWidth; ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; ctx.circle(x, y, size); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); } }, { key: '_drawImageAtPosition', value: function _drawImageAtPosition(ctx) { if (this.imageObj.width != 0) { // draw the image ctx.globalAlpha = 1; // draw shadow if enabled this.enableShadow(ctx); ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); // disable shadows for other elements. this.disableShadow(ctx); } } }, { key: '_drawImageLabel', value: function _drawImageLabel(ctx, x, y, selected) { var yLabel; var offset = 0; if (this.height !== undefined) { offset = this.height * 0.5; var labelDimensions = this.labelModule.getTextSize(ctx); if (labelDimensions.lineCount >= 1) { offset += labelDimensions.height / 2; } } yLabel = y + offset; if (this.options.label) { this.labelOffset = offset; } this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); } }]); return CircleImageBase; })(_utilNodeBase2['default']); exports['default'] = CircleImageBase; module.exports = exports['default']; /***/ }, /* 68 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilCircleImageBase = __webpack_require__(67); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); var CircularImage = (function (_CircleImageBase) { function CircularImage(options, body, labelModule, imageObj) { _classCallCheck(this, CircularImage); _get(Object.getPrototypeOf(CircularImage.prototype), 'constructor', this).call(this, options, body, labelModule); this.imageObj = imageObj; this._swapToImageResizeWhenImageLoaded = true; } _inherits(CircularImage, _CircleImageBase); _createClass(CircularImage, [{ key: 'resize', value: function resize() { if (this.imageObj.src === undefined || this.imageObj.width === undefined || this.imageObj.height === undefined) { if (!this.width) { var diameter = this.options.size * 2; this.width = diameter; this.height = diameter; this._swapToImageResizeWhenImageLoaded = true; this.radius = 0.5 * this.width; } } else { if (this._swapToImageResizeWhenImageLoaded) { this.width = undefined; this.height = undefined; this._swapToImageResizeWhenImageLoaded = false; } this._resizeImage(); } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(); this.left = x - this.width / 2; this.top = y - this.height / 2; var size = Math.min(0.5 * this.height, 0.5 * this.width); this._drawRawCircle(ctx, x, y, selected, hover, size); ctx.save(); ctx.circle(x, y, size); ctx.stroke(); ctx.clip(); this._drawImageAtPosition(ctx); ctx.restore(); this._drawImageLabel(ctx, x, y, selected); this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.boundingBox.top = y - this.options.size; this.boundingBox.left = x - this.options.size; this.boundingBox.right = x + this.options.size; this.boundingBox.bottom = y + this.options.size; this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); return this._distanceToBorder(angle); } }]); return CircularImage; })(_utilCircleImageBase2['default']); exports['default'] = CircularImage; module.exports = exports['default']; /***/ }, /* 69 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var Database = (function (_NodeBase) { function Database(options, body, labelModule) { _classCallCheck(this, Database); _get(Object.getPrototypeOf(Database.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Database, _NodeBase); _createClass(Database, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { var margin = 5; var textSize = this.labelModule.getTextSize(ctx, selected); var size = textSize.width + 2 * margin; this.width = size; this.height = size; this.radius = 0.5 * this.width; } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx, selected); this.left = x - this.width / 2; this.top = y - this.height / 2; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); this.updateBoundingBox(x, y); this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.left = x - this.width * 0.5; this.top = y - this.height * 0.5; this.boundingBox.left = this.left; this.boundingBox.top = this.top; this.boundingBox.bottom = this.top + this.height; this.boundingBox.right = this.left + this.width; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); var a = this.width / 2; var b = this.height / 2; var w = Math.sin(angle) * a; var h = Math.cos(angle) * b; return a * b / Math.sqrt(w * w + h * h); } }]); return Database; })(_utilNodeBase2['default']); exports['default'] = Database; module.exports = exports['default']; /***/ }, /* 70 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilShapeBase = __webpack_require__(71); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); var Diamond = (function (_ShapeBase) { function Diamond(options, body, labelModule) { _classCallCheck(this, Diamond); _get(Object.getPrototypeOf(Diamond.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Diamond, _ShapeBase); _createClass(Diamond, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this._drawShape(ctx, 'diamond', 4, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { return this._distanceToBorder(angle); } }]); return Diamond; })(_utilShapeBase2['default']); exports['default'] = Diamond; module.exports = exports['default']; /***/ }, /* 71 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var ShapeBase = (function (_NodeBase) { function ShapeBase(options, body, labelModule) { _classCallCheck(this, ShapeBase); _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(ShapeBase, _NodeBase); _createClass(ShapeBase, [{ key: '_resizeShape', value: function _resizeShape() { if (this.width === undefined) { var size = 2 * this.options.size; this.width = size; this.height = size; this.radius = 0.5 * this.width; } } }, { key: '_drawShape', value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { this._resizeShape(); this.left = x - this.width / 2; this.top = y - this.height / 2; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; ctx.lineWidth = selected ? selectionLineWidth : borderWidth; ctx.lineWidth /= this.body.view.scale; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; ctx[shape](x, y, this.options.size); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); if (this.options.label !== undefined) { var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); } this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.boundingBox.top = y - this.options.size; this.boundingBox.left = x - this.options.size; this.boundingBox.right = x + this.options.size; this.boundingBox.bottom = y + this.options.size; if (this.options.label !== undefined) { this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + 3); } } }]); return ShapeBase; })(_utilNodeBase2['default']); exports['default'] = ShapeBase; module.exports = exports['default']; /***/ }, /* 72 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilShapeBase = __webpack_require__(71); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); var Dot = (function (_ShapeBase) { function Dot(options, body, labelModule) { _classCallCheck(this, Dot); _get(Object.getPrototypeOf(Dot.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Dot, _ShapeBase); _createClass(Dot, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this._drawShape(ctx, 'circle', 2, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { return this.options.size + this.options.borderWidth; } }]); return Dot; })(_utilShapeBase2['default']); exports['default'] = Dot; module.exports = exports['default']; /***/ }, /* 73 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var Ellipse = (function (_NodeBase) { function Ellipse(options, body, labelModule) { _classCallCheck(this, Ellipse); _get(Object.getPrototypeOf(Ellipse.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Ellipse, _NodeBase); _createClass(Ellipse, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { var textSize = this.labelModule.getTextSize(ctx, selected); this.width = textSize.width * 1.5; this.height = textSize.height * 2; if (this.width < this.height) { this.width = this.height; } this.radius = 0.5 * this.width; } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx, selected); this.left = x - this.width * 0.5; this.top = y - this.height * 0.5; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; ctx.lineWidth = selected ? selectionLineWidth : borderWidth; ctx.lineWidth /= this.body.view.scale; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; ctx.ellipse(this.left, this.top, this.width, this.height); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); this.updateBoundingBox(x, y); this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.left = x - this.width * 0.5; this.top = y - this.height * 0.5; this.boundingBox.left = this.left; this.boundingBox.top = this.top; this.boundingBox.bottom = this.top + this.height; this.boundingBox.right = this.left + this.width; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); var a = this.width * 0.5; var b = this.height * 0.5; var w = Math.sin(angle) * a; var h = Math.cos(angle) * b; return a * b / Math.sqrt(w * w + h * h); } }]); return Ellipse; })(_utilNodeBase2['default']); exports['default'] = Ellipse; module.exports = exports['default']; /***/ }, /* 74 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var Icon = (function (_NodeBase) { function Icon(options, body, labelModule) { _classCallCheck(this, Icon); _get(Object.getPrototypeOf(Icon.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Icon, _NodeBase); _createClass(Icon, [{ key: 'resize', value: function resize(ctx) { if (this.width === undefined) { var margin = 5; var iconSize = { width: Number(this.options.icon.size), height: Number(this.options.icon.size) }; this.width = iconSize.width + 2 * margin; this.height = iconSize.height + 2 * margin; this.radius = 0.5 * this.width; } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx); this.options.icon.size = this.options.icon.size || 50; this.left = x - this.width * 0.5; this.top = y - this.height * 0.5; this._icon(ctx, x, y, selected); if (this.options.label !== undefined) { var iconTextSpacing = 5; this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); } this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.boundingBox.top = y - this.options.icon.size * 0.5; this.boundingBox.left = x - this.options.icon.size * 0.5; this.boundingBox.right = x + this.options.icon.size * 0.5; this.boundingBox.bottom = y + this.options.icon.size * 0.5; if (this.options.label !== undefined) { var iconTextSpacing = 5; this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing); } } }, { key: '_icon', value: function _icon(ctx, x, y, selected) { var iconSize = Number(this.options.icon.size); if (this.options.icon.code !== undefined) { ctx.font = (selected ? 'bold ' : '') + iconSize + 'px ' + this.options.icon.face; // draw icon ctx.fillStyle = this.options.icon.color || 'black'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // draw shadow if enabled this.enableShadow(ctx); ctx.fillText(this.options.icon.code, x, y); // disable shadows for other elements. this.disableShadow(ctx); } else { console.error('When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally.'); } } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); this._distanceToBorder(angle); } }]); return Icon; })(_utilNodeBase2['default']); exports['default'] = Icon; module.exports = exports['default']; /***/ }, /* 75 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilCircleImageBase = __webpack_require__(67); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); var Image = (function (_CircleImageBase) { function Image(options, body, labelModule, imageObj) { _classCallCheck(this, Image); _get(Object.getPrototypeOf(Image.prototype), 'constructor', this).call(this, options, body, labelModule); this.imageObj = imageObj; } _inherits(Image, _CircleImageBase); _createClass(Image, [{ key: 'resize', value: function resize() { this._resizeImage(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(); this.left = x - this.width / 2; this.top = y - this.height / 2; this._drawImageAtPosition(ctx); this._drawImageLabel(ctx, x, y, selected || hover); this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.left = x - this.width / 2; this.top = y - this.height / 2; this.boundingBox.top = this.top; this.boundingBox.left = this.left; this.boundingBox.right = this.left + this.width; this.boundingBox.bottom = this.top + this.height; if (this.options.label !== undefined) { this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); } } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); var a = this.width / 2; var b = this.height / 2; var w = Math.sin(angle) * a; var h = Math.cos(angle) * b; return a * b / Math.sqrt(w * w + h * h); } }]); return Image; })(_utilCircleImageBase2['default']); exports['default'] = Image; module.exports = exports['default']; /***/ }, /* 76 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilShapeBase = __webpack_require__(71); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); var Square = (function (_ShapeBase) { function Square(options, body, labelModule) { _classCallCheck(this, Square); _get(Object.getPrototypeOf(Square.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Square, _ShapeBase); _createClass(Square, [{ key: 'resize', value: function resize() { this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this._drawShape(ctx, 'square', 2, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); return this._distanceToBorder(angle); } }]); return Square; })(_utilShapeBase2['default']); exports['default'] = Square; module.exports = exports['default']; /***/ }, /* 77 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilShapeBase = __webpack_require__(71); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); var Star = (function (_ShapeBase) { function Star(options, body, labelModule) { _classCallCheck(this, Star); _get(Object.getPrototypeOf(Star.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Star, _ShapeBase); _createClass(Star, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this._drawShape(ctx, 'star', 4, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { return this._distanceToBorder(angle); } }]); return Star; })(_utilShapeBase2['default']); exports['default'] = Star; module.exports = exports['default']; /***/ }, /* 78 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilNodeBase = __webpack_require__(65); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); var Text = (function (_NodeBase) { function Text(options, body, labelModule) { _classCallCheck(this, Text); _get(Object.getPrototypeOf(Text.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Text, _NodeBase); _createClass(Text, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { var margin = 5; var textSize = this.labelModule.getTextSize(ctx, selected); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; this.radius = 0.5 * this.width; } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx, selected || hover); this.left = x - this.width / 2; this.top = y - this.height / 2; // draw shadow if enabled this.enableShadow(ctx); this.labelModule.draw(ctx, x, y, selected || hover); // disable shadows for other elements. this.disableShadow(ctx); this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { this.left = x - this.width / 2; this.top = y - this.height / 2; this.boundingBox.top = this.top; this.boundingBox.left = this.left; this.boundingBox.right = this.left + this.width; this.boundingBox.bottom = this.top + this.height; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); return this._distanceToBorder(angle); } }]); return Text; })(_utilNodeBase2['default']); exports['default'] = Text; module.exports = exports['default']; /***/ }, /* 79 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilShapeBase = __webpack_require__(71); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); var Triangle = (function (_ShapeBase) { function Triangle(options, body, labelModule) { _classCallCheck(this, Triangle); _get(Object.getPrototypeOf(Triangle.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(Triangle, _ShapeBase); _createClass(Triangle, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this._drawShape(ctx, 'triangle', 3, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { return this._distanceToBorder(angle); } }]); return Triangle; })(_utilShapeBase2['default']); exports['default'] = Triangle; module.exports = exports['default']; /***/ }, /* 80 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilShapeBase = __webpack_require__(71); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); var TriangleDown = (function (_ShapeBase) { function TriangleDown(options, body, labelModule) { _classCallCheck(this, TriangleDown); _get(Object.getPrototypeOf(TriangleDown.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(TriangleDown, _ShapeBase); _createClass(TriangleDown, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { return this._distanceToBorder(angle); } }]); return TriangleDown; })(_utilShapeBase2['default']); exports['default'] = TriangleDown; module.exports = exports['default']; /***/ }, /* 81 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _componentsEdge = __webpack_require__(82); var _componentsEdge2 = _interopRequireDefault(_componentsEdge); var _componentsSharedLabel = __webpack_require__(63); var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); var util = __webpack_require__(1); var DataSet = __webpack_require__(8); var DataView = __webpack_require__(10); var EdgesHandler = (function () { function EdgesHandler(body, images, groups) { var _this = this; _classCallCheck(this, EdgesHandler); this.body = body; this.images = images; this.groups = groups; // create the edge API in the body container this.body.functions.createEdge = this.create.bind(this); this.edgesListeners = { add: function add(event, params) { _this.add(params.items); }, update: function update(event, params) { _this.update(params.items); }, remove: function remove(event, params) { _this.remove(params.items); } }; this.options = {}; this.defaultOptions = { arrows: { to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} middle: { enabled: false, scaleFactor: 1 }, from: { enabled: false, scaleFactor: 1 } }, color: { color: '#848484', highlight: '#848484', hover: '#848484', inherit: 'from', opacity: 1 }, dashes: false, font: { color: '#343434', size: 14, // px face: 'arial', background: 'none', strokeWidth: 2, // px strokeColor: '#ffffff', align: 'horizontal' }, hidden: false, hoverWidth: 1.5, label: undefined, length: undefined, physics: true, scaling: { min: 1, max: 15, label: { enabled: true, min: 14, max: 30, maxVisible: 30, drawThreshold: 5 }, customScalingFunction: function customScalingFunction(min, max, total, value) { if (max === min) { return 0.5; } else { var scale = 1 / (max - min); return Math.max(0, (value - min) * scale); } } }, selectionWidth: 1.5, selfReferenceSize: 20, shadow: { enabled: false, size: 10, x: 5, y: 5 }, smooth: { enabled: true, type: 'dynamic', roundness: 0.5 }, title: undefined, width: 1, value: undefined }; util.extend(this.options, this.defaultOptions); this.bindEventListeners(); } _createClass(EdgesHandler, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this2 = this; // this allows external modules to force all dynamic curves to turn static. this.body.emitter.on('_forceDisableDynamicCurves', function (type) { if (type === 'dynamic') { type = 'continuous'; } var emitChange = false; for (var edgeId in _this2.body.edges) { if (_this2.body.edges.hasOwnProperty(edgeId)) { var edge = _this2.body.edges[edgeId]; var edgeData = _this2.body.data.edges._data[edgeId]; // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. // this is because a change in the global would not affect these curves. if (edgeData !== undefined) { var edgeOptions = edgeData.smooth; if (edgeOptions !== undefined) { if (edgeOptions.enabled === true && edgeOptions.type === 'dynamic') { if (type === undefined) { edge.setOptions({ smooth: false }); } else { edge.setOptions({ smooth: { type: type } }); } emitChange = true; } } } } } if (emitChange === true) { _this2.body.emitter.emit('_dataChanged'); } }); // this is called when options of EXISTING nodes or edges have changed. this.body.emitter.on('_dataUpdated', function () { _this2.reconnectEdges(); _this2.markAllEdgesAsDirty(); }); // refresh the edges. Used when reverting from hierarchical layout this.body.emitter.on('refreshEdges', this.refresh.bind(this)); this.body.emitter.on('refresh', this.refresh.bind(this)); this.body.emitter.on('destroy', function () { delete _this2.body.functions.createEdge; delete _this2.edgesListeners.add; delete _this2.edgesListeners.update; delete _this2.edgesListeners.remove; delete _this2.edgesListeners; }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { // use the parser from the Edge class to fill in all shorthand notations _componentsEdge2['default'].parseOptions(this.options, options); // hanlde multiple input cases for color if (options.color !== undefined) { this.markAllEdgesAsDirty(); } // update smooth settings in all edges var dataChanged = false; if (options.smooth !== undefined) { for (var edgeId in this.body.edges) { if (this.body.edges.hasOwnProperty(edgeId)) { dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; } } } // update fonts in all edges if (options.font !== undefined) { // use the parser from the Label class to fill in all shorthand notations _componentsSharedLabel2['default'].parseOptions(this.options.font, options); for (var edgeId in this.body.edges) { if (this.body.edges.hasOwnProperty(edgeId)) { this.body.edges[edgeId].updateLabelModule(); } } } // update the state of the variables if needed if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { this.body.emitter.emit('_dataChanged'); } } } }, { key: 'setData', /** * Load edges by reading the data table * @param {Array | DataSet | DataView} edges The data containing the edges. * @private * @private */ value: function setData(edges) { var _this3 = this; var doNotEmit = arguments[1] === undefined ? false : arguments[1]; var oldEdgesData = this.body.data.edges; if (edges instanceof DataSet || edges instanceof DataView) { this.body.data.edges = edges; } else if (Array.isArray(edges)) { this.body.data.edges = new DataSet(); this.body.data.edges.add(edges); } else if (!edges) { this.body.data.edges = new DataSet(); } else { throw new TypeError('Array or DataSet expected'); } // TODO: is this null or undefined or false? if (oldEdgesData) { // unsubscribe from old dataset util.forEach(this.edgesListeners, function (callback, event) { oldEdgesData.off(event, callback); }); } // remove drawn edges this.body.edges = {}; // TODO: is this null or undefined or false? if (this.body.data.edges) { // subscribe to new dataset util.forEach(this.edgesListeners, function (callback, event) { _this3.body.data.edges.on(event, callback); }); // draw all new nodes var ids = this.body.data.edges.getIds(); this.add(ids, true); } if (doNotEmit === false) { this.body.emitter.emit('_dataChanged'); } } }, { key: 'add', /** * Add edges * @param {Number[] | String[]} ids * @private */ value: function add(ids) { var doNotEmit = arguments[1] === undefined ? false : arguments[1]; var edges = this.body.edges; var edgesData = this.body.data.edges; for (var i = 0; i < ids.length; i++) { var id = ids[i]; var oldEdge = edges[id]; if (oldEdge) { oldEdge.disconnect(); } var data = edgesData.get(id, { 'showInternalIds': true }); edges[id] = this.create(data); } if (doNotEmit === false) { this.body.emitter.emit('_dataChanged'); } } }, { key: 'update', /** * Update existing edges, or create them when not yet existing * @param {Number[] | String[]} ids * @private */ value: function update(ids) { var edges = this.body.edges; var edgesData = this.body.data.edges; var dataChanged = false; for (var i = 0; i < ids.length; i++) { var id = ids[i]; var data = edgesData.get(id); var edge = edges[id]; if (edge === null) { // update edge edge.disconnect(); dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. edge.connect(); } else { // create edge this.body.edges[id] = this.create(data); dataChanged = true; } } if (dataChanged === true) { this.body.emitter.emit('_dataChanged'); } else { this.body.emitter.emit('_dataUpdated'); } } }, { key: 'remove', /** * Remove existing edges. Non existing ids will be ignored * @param {Number[] | String[]} ids * @private */ value: function remove(ids) { var edges = this.body.edges; for (var i = 0; i < ids.length; i++) { var id = ids[i]; var edge = edges[id]; if (edge !== undefined) { if (edge.via != null) { delete this.body.supportNodes[edge.via.id]; } edge.disconnect(); delete edges[id]; } } this.body.emitter.emit('_dataChanged'); } }, { key: 'refresh', value: function refresh() { var edges = this.body.edges; for (var edgeId in edges) { var edge = undefined; if (edges.hasOwnProperty(edgeId)) { edge = edges[edgeId]; } var data = this.body.data.edges._data[edgeId]; if (edge !== undefined && data !== undefined) { edge.setOptions(data); } } } }, { key: 'create', value: function create(properties) { return new _componentsEdge2['default'](properties, this.body, this.options); } }, { key: 'markAllEdgesAsDirty', value: function markAllEdgesAsDirty() { for (var edgeId in this.body.edges) { this.body.edges[edgeId].edgeType.colorDirty = true; } } }, { key: 'reconnectEdges', /** * Reconnect all edges * @private */ value: function reconnectEdges() { var id; var nodes = this.body.nodes; var edges = this.body.edges; for (id in nodes) { if (nodes.hasOwnProperty(id)) { nodes[id].edges = []; } } for (id in edges) { if (edges.hasOwnProperty(id)) { var edge = edges[id]; edge.from = null; edge.to = null; edge.connect(); } } } }, { key: 'getConnectedNodes', value: function getConnectedNodes(edgeId) { var nodeList = []; if (this.body.edges[edgeId] !== undefined) { var edge = this.body.edges[edgeId]; if (edge.fromId) { nodeList.push(edge.fromId); } if (edge.toId) { nodeList.push(edge.toId); } } return nodeList; } }]); return EdgesHandler; })(); exports['default'] = EdgesHandler; module.exports = exports['default']; /***/ }, /* 82 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _sharedLabel = __webpack_require__(63); var _sharedLabel2 = _interopRequireDefault(_sharedLabel); var _edgesBezierEdgeDynamic = __webpack_require__(83); var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); var _edgesBezierEdgeStatic = __webpack_require__(86); var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); var _edgesStraightEdge = __webpack_require__(87); var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); var util = __webpack_require__(1); /** * @class Edge * * A edge connects two nodes * @param {Object} properties Object with options. Must contain * At least options from and to. * Available options: from (number), * to (number), label (string, color (string), * width (number), style (string), * length (number), title (string) * @param {Network} network A Network object, used to find and edge to * nodes. * @param {Object} constants An object with default values for * example for the color */ var Edge = (function () { function Edge(options, body, globalOptions) { _classCallCheck(this, Edge); if (body === undefined) { throw 'No body provided'; } this.options = util.bridgeObject(globalOptions); this.body = body; // initialize variables this.id = undefined; this.fromId = undefined; this.toId = undefined; this.selected = false; this.hover = false; this.labelDirty = true; this.colorDirty = true; this.baseWidth = this.options.width; this.baseFontSize = this.options.font.size; this.from = undefined; // a node this.to = undefined; // a node this.edgeType = undefined; this.connected = false; this.labelModule = new _sharedLabel2['default'](this.body, this.options); this.setOptions(options); } _createClass(Edge, [{ key: 'setOptions', /** * Set or overwrite options for the edge * @param {Object} options an object with options * @param doNotEmit */ value: function setOptions(options) { if (!options) { return; } this.colorDirty = true; Edge.parseOptions(this.options, options, true); if (options.id !== undefined) { this.id = options.id; } if (options.from !== undefined) { this.fromId = options.from; } if (options.to !== undefined) { this.toId = options.to; } if (options.title !== undefined) { this.title = options.title; } if (options.value !== undefined) { options.value = parseFloat(options.value); } // update label Module this.updateLabelModule(); var dataChanged = this.updateEdgeType(); // if anything has been updates, reset the selection width and the hover width this._setInteractionWidths(); // A node is connected when it has a from and to node that both exist in the network.body.nodes. this.connect(); if (options.hidden !== undefined || options.physics !== undefined) { dataChanged = true; } return dataChanged; } }, { key: 'updateLabelModule', /** * update the options in the label module */ value: function updateLabelModule() { this.labelModule.setOptions(this.options, true); if (this.labelModule.baseSize !== undefined) { this.baseFontSize = this.labelModule.baseSize; } } }, { key: 'updateEdgeType', /** * update the edge type, set the options * @returns {boolean} */ value: function updateEdgeType() { var dataChanged = false; var changeInType = true; if (this.edgeType !== undefined) { if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type === 'dynamic') { changeInType = false; } if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type !== 'dynamic') { changeInType = false; } if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { changeInType = false; } if (changeInType === true) { dataChanged = this.edgeType.cleanup(); } } if (changeInType === true) { if (this.options.smooth.enabled === true) { if (this.options.smooth.type === 'dynamic') { dataChanged = true; this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); } else { this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); } } else { this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); } } else { // if nothing changes, we just set the options. this.edgeType.setOptions(this.options); } return dataChanged; } }, { key: 'togglePhysics', /** * Enable or disable the physics. * @param status */ value: function togglePhysics(status) { this.options.physics = status; this.edgeType.togglePhysics(status); } }, { key: 'connect', /** * Connect an edge to its nodes */ value: function connect() { this.disconnect(); this.from = this.body.nodes[this.fromId] || undefined; this.to = this.body.nodes[this.toId] || undefined; this.connected = this.from !== undefined && this.to !== undefined; if (this.connected === true) { this.from.attachEdge(this); this.to.attachEdge(this); } else { if (this.from) { this.from.detachEdge(this); } if (this.to) { this.to.detachEdge(this); } } this.edgeType.connect(); } }, { key: 'disconnect', /** * Disconnect an edge from its nodes */ value: function disconnect() { if (this.from) { this.from.detachEdge(this); this.from = undefined; } if (this.to) { this.to.detachEdge(this); this.to = undefined; } this.connected = false; } }, { key: 'getTitle', /** * get the title of this edge. * @return {string} title The title of the edge, or undefined when no title * has been set. */ value: function getTitle() { return this.title; } }, { key: 'isSelected', /** * check if this node is selecte * @return {boolean} selected True if node is selected, else false */ value: function isSelected() { return this.selected; } }, { key: 'getValue', /** * Retrieve the value of the edge. Can be undefined * @return {Number} value */ value: function getValue() { return this.options.value; } }, { key: 'setValueRange', /** * Adjust the value range of the edge. The edge will adjust it's width * based on its value. * @param {Number} min * @param {Number} max * @param total */ value: function setValueRange(min, max, total) { if (this.options.value !== undefined) { var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); var widthDiff = this.options.scaling.max - this.options.scaling.min; if (this.options.scaling.label.enabled === true) { var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; this.options.font.size = this.options.scaling.label.min + scale * fontDiff; } this.options.width = this.options.scaling.min + scale * widthDiff; } else { this.options.width = this.baseWidth; this.options.font.size = this.baseFontSize; } this._setInteractionWidths(); } }, { key: '_setInteractionWidths', value: function _setInteractionWidths() { if (typeof this.options.hoverWidth === 'function') { this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); } else { this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; } if (typeof this.options.selectionWidth === 'function') { this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); } else { this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; } } }, { key: 'draw', /** * Redraw a edge * Draw this edge in the given canvas * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); * @param {CanvasRenderingContext2D} ctx */ value: function draw(ctx) { var via = this.edgeType.drawLine(ctx, this.selected, this.hover); this.drawArrows(ctx, via); this.drawLabel(ctx, via); } }, { key: 'drawArrows', value: function drawArrows(ctx, viaNode) { if (this.options.arrows.from.enabled === true) { this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); } if (this.options.arrows.middle.enabled === true) { this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); } if (this.options.arrows.to.enabled === true) { this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); } } }, { key: 'drawLabel', value: function drawLabel(ctx, viaNode) { if (this.options.label !== undefined) { // set style var node1 = this.from; var node2 = this.to; var selected = this.from.selected || this.to.selected || this.selected; if (node1.id != node2.id) { this.labelModule.pointToSelf = false; var point = this.edgeType.getPoint(0.5, viaNode); ctx.save(); // if the label has to be rotated: if (this.options.font.align !== 'horizontal') { this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); ctx.translate(point.x, this.labelModule.size.yLine); this._rotateForLabelAlignment(ctx); } // draw the label this.labelModule.draw(ctx, point.x, point.y, selected); ctx.restore(); } else { // Ignore the orientations. this.labelModule.pointToSelf = true; var x, y; var radius = this.options.selfReferenceSize; if (node1.shape.width > node1.shape.height) { x = node1.x + node1.shape.width * 0.5; y = node1.y - radius; } else { x = node1.x + radius; y = node1.y - node1.shape.height * 0.5; } point = this._pointOnCircle(x, y, radius, 0.125); this.labelModule.draw(ctx, point.x, point.y, selected); } } } }, { key: 'isOverlappingWith', /** * Check if this object is overlapping with the provided object * @param {Object} obj an object with parameters left, top * @return {boolean} True if location is located on the edge */ value: function isOverlappingWith(obj) { if (this.connected) { var distMax = 10; var xFrom = this.from.x; var yFrom = this.from.y; var xTo = this.to.x; var yTo = this.to.y; var xObj = obj.left; var yObj = obj.top; var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); return dist < distMax; } else { return false; } } }, { key: '_rotateForLabelAlignment', /** * Rotates the canvas so the text is most readable * @param {CanvasRenderingContext2D} ctx * @private */ value: function _rotateForLabelAlignment(ctx) { var dy = this.from.y - this.to.y; var dx = this.from.x - this.to.x; var angleInDegrees = Math.atan2(dy, dx); // rotate so label it is readable if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { angleInDegrees = angleInDegrees + Math.PI; } ctx.rotate(angleInDegrees); } }, { key: '_pointOnCircle', /** * Get a point on a circle * @param {Number} x * @param {Number} y * @param {Number} radius * @param {Number} percentage. Value between 0 (line start) and 1 (line end) * @return {Object} point * @private */ value: function _pointOnCircle(x, y, radius, percentage) { var angle = percentage * 2 * Math.PI; return { x: x + radius * Math.cos(angle), y: y - radius * Math.sin(angle) }; } }, { key: 'select', value: function select() { this.selected = true; } }, { key: 'unselect', value: function unselect() { this.selected = false; } }], [{ key: 'parseOptions', value: function parseOptions(parentOptions, newOptions) { var allowDeletion = arguments[2] === undefined ? false : arguments[2]; var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; // only deep extend the items in the field array. These do not have shorthand. util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); util.mergeOptions(parentOptions, newOptions, 'smooth'); util.mergeOptions(parentOptions, newOptions, 'shadow'); if (newOptions.dashes !== undefined && newOptions.dashes !== null) { parentOptions.dashes = newOptions.dashes; } else if (allowDeletion === true && newOptions.dashes === null) { parentOptions.dashes = undefined; delete parentOptions.dashes; } // set the scaling newOptions if (newOptions.scaling !== undefined && newOptions.scaling !== null) { if (newOptions.scaling.min !== undefined) { parentOptions.scaling.min = newOptions.scaling.min; } if (newOptions.scaling.max !== undefined) { parentOptions.scaling.max = newOptions.scaling.max; } util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); } else if (allowDeletion === true && newOptions.scaling === null) { parentOptions.scaling = undefined; delete parentOptions.scaling; } // hanlde multiple input cases for arrows if (newOptions.arrows !== undefined && newOptions.arrows !== null) { if (typeof newOptions.arrows === 'string') { var arrows = newOptions.arrows.toLowerCase(); if (arrows.indexOf('to') != -1) { parentOptions.arrows.to.enabled = true; } if (arrows.indexOf('middle') != -1) { parentOptions.arrows.middle.enabled = true; } if (arrows.indexOf('from') != -1) { parentOptions.arrows.from.enabled = true; } } else if (typeof newOptions.arrows === 'object') { util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); } else { throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); } } else if (allowDeletion === true && newOptions.arrows === null) { parentOptions.arrows = undefined; delete parentOptions.arrows; } // hanlde multiple input cases for color if (newOptions.color !== undefined && newOptions.color !== null) { if (util.isString(newOptions.color)) { parentOptions.color.color = newOptions.color; parentOptions.color.highlight = newOptions.color; parentOptions.color.hover = newOptions.color; parentOptions.color.inherit = false; } else { var colorsDefined = false; if (newOptions.color.color !== undefined) { parentOptions.color.color = newOptions.color.color;colorsDefined = true; } if (newOptions.color.highlight !== undefined) { parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; } if (newOptions.color.hover !== undefined) { parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; } if (newOptions.color.inherit !== undefined) { parentOptions.color.inherit = newOptions.color.inherit; } if (newOptions.color.opacity !== undefined) { parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); } if (newOptions.color.inherit === undefined && colorsDefined === true) { parentOptions.color.inherit = false; } } } else if (allowDeletion === true && newOptions.color === null) { parentOptions.color = undefined; delete parentOptions.color; } // handle the font settings if (newOptions.font !== undefined) { _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); } } }]); return Edge; })(); exports['default'] = Edge; module.exports = exports['default']; /***/ }, /* 83 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilBezierEdgeBase = __webpack_require__(84); var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); var BezierEdgeDynamic = (function (_BezierEdgeBase) { function BezierEdgeDynamic(options, body, labelModule) { _classCallCheck(this, BezierEdgeDynamic); //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), 'constructor', this).call(this, options, body, labelModule); // --> this calls the setOptions below } _inherits(BezierEdgeDynamic, _BezierEdgeBase); _createClass(BezierEdgeDynamic, [{ key: 'setOptions', value: function setOptions(options) { this.options = options; this.id = this.options.id; this.setupSupportNode(); this.connect(); } }, { key: 'connect', value: function connect() { this.from = this.body.nodes[this.options.from]; this.to = this.body.nodes[this.options.to]; if (this.from === undefined || this.to === undefined) { this.via.setOptions({ physics: false }); } else { // fix weird behaviour where a selfreferencing node has physics enabled if (this.from.id === this.to.id) { this.via.setOptions({ physics: false }); } else { this.via.setOptions({ physics: true }); } } } }, { key: 'cleanup', value: function cleanup() { if (this.via !== undefined) { delete this.body.nodes[this.via.id]; this.via = undefined; return true; } return false; } }, { key: 'togglePhysics', value: function togglePhysics(status) { this.via.setOptions({ physics: status }); this.positionBezierNode(); } }, { key: 'setupSupportNode', /** * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but * are used for the force calculation. * * The changed data is not called, if needed, it is returned by the main edge constructor. * @private */ value: function setupSupportNode() { if (this.via === undefined) { var nodeId = 'edgeId:' + this.id; var node = this.body.functions.createNode({ id: nodeId, shape: 'circle', physics: true, hidden: true }); this.body.nodes[nodeId] = node; this.via = node; this.via.parentEdgeId = this.id; this.positionBezierNode(); } } }, { key: 'positionBezierNode', value: function positionBezierNode() { if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { this.via.x = 0.5 * (this.from.x + this.to.x); this.via.y = 0.5 * (this.from.y + this.to.y); } else if (this.via !== undefined) { this.via.x = 0; this.via.y = 0; } } }, { key: '_line', /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx * @private */ value: function _line(ctx) { // draw a straight line ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); // draw shadow if enabled this.enableShadow(ctx); ctx.stroke(); this.disableShadow(ctx); return this.via; } }, { key: 'getPoint', /** * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way * @param percentage * @param via * @returns {{x: number, y: number}} * @private */ value: function getPoint(percentage) { var t = percentage; var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; return { x: x, y: y }; } }, { key: '_findBorderPosition', value: function _findBorderPosition(nearNode, ctx) { return this._findBorderPositionBezier(nearNode, ctx, this.via); } }, { key: '_getDistanceToEdge', value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); } }]); return BezierEdgeDynamic; })(_utilBezierEdgeBase2['default']); exports['default'] = BezierEdgeDynamic; module.exports = exports['default']; /***/ }, /* 84 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _EdgeBase2 = __webpack_require__(85); var _EdgeBase3 = _interopRequireDefault(_EdgeBase2); var BezierEdgeBase = (function (_EdgeBase) { function BezierEdgeBase(options, body, labelModule) { _classCallCheck(this, BezierEdgeBase); _get(Object.getPrototypeOf(BezierEdgeBase.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(BezierEdgeBase, _EdgeBase); _createClass(BezierEdgeBase, [{ key: '_findBorderPositionBezier', /** * This function uses binary search to look for the point where the bezier curve crosses the border of the node. * * @param nearNode * @param ctx * @param viaNode * @param nearNode * @param ctx * @param viaNode * @param nearNode * @param ctx * @param viaNode */ value: function _findBorderPositionBezier(nearNode, ctx) { var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; var maxIterations = 10; var iteration = 0; var low = 0; var high = 1; var pos, angle, distanceToBorder, distanceToPoint, difference; var threshold = 0.2; var node = this.to; var from = false; if (nearNode.id === this.from.id) { node = this.from; from = true; } while (low <= high && iteration < maxIterations) { var middle = (low + high) * 0.5; pos = this.getPoint(middle, viaNode); angle = Math.atan2(node.y - pos.y, node.x - pos.x); distanceToBorder = node.distanceToBorder(ctx, angle); distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); difference = distanceToBorder - distanceToPoint; if (Math.abs(difference) < threshold) { break; // found } else if (difference < 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. if (from === false) { low = middle; } else { high = middle; } } else { if (from === false) { high = middle; } else { low = middle; } } iteration++; } pos.t = middle; return pos; } }, { key: '_getDistanceToBezierEdge', /** * Calculate the distance between a point (x3,y3) and a line segment from * (x1,y1) to (x2,y2). * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @private */ value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { // x3,y3 is the point var xVia = undefined, yVia = undefined; xVia = via.x; yVia = via.y; var minDistance = 1000000000; var distance = undefined; var i = undefined, t = undefined, x = undefined, y = undefined; var lastX = x1; var lastY = y1; for (i = 1; i < 10; i++) { t = 0.1 * i; x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; if (i > 0) { distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); minDistance = distance < minDistance ? distance : minDistance; } lastX = x; lastY = y; } return minDistance; } }]); return BezierEdgeBase; })(_EdgeBase3['default']); exports['default'] = BezierEdgeBase; module.exports = exports['default']; /***/ }, /* 85 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); var EdgeBase = (function () { function EdgeBase(options, body, labelModule) { _classCallCheck(this, EdgeBase); this.body = body; this.labelModule = labelModule; this.setOptions(options); this.colorDirty = true; this.color = {}; this.selectionWidth = 2; this.hoverWidth = 1.5; } _createClass(EdgeBase, [{ key: 'connect', value: function connect() {} }, { key: 'cleanup', value: function cleanup() { return false; } }, { key: 'setOptions', value: function setOptions(options) { this.options = options; this.from = this.body.nodes[this.options.from]; this.to = this.body.nodes[this.options.to]; this.id = this.options.id; } }, { key: 'togglePhysics', /** * overloadable if the shape has to toggle the via node to disabled * @param status */ value: function togglePhysics(status) {} }, { key: 'drawLine', /** * Redraw a edge as a line * Draw this edge in the given canvas * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); * @param {CanvasRenderingContext2D} ctx * @private */ value: function drawLine(ctx, selected, hover) { // set style ctx.strokeStyle = this.getColor(ctx, selected, hover); ctx.lineWidth = this.getLineWidth(selected, hover); var via = undefined; if (this.options.dashes !== false) { via = this._drawDashedLine(ctx); } else { via = this._drawLine(ctx); } return via; } }, { key: '_drawLine', value: function _drawLine(ctx) { var via = undefined; if (this.from != this.to) { // draw line via = this._line(ctx); } else { var _getCircleData2 = this._getCircleData(ctx); var _getCircleData22 = _slicedToArray(_getCircleData2, 3); var x = _getCircleData22[0]; var y = _getCircleData22[1]; var radius = _getCircleData22[2]; this._circle(ctx, x, y, radius); } return via; } }, { key: '_drawDashedLine', value: function _drawDashedLine(ctx) { var via = undefined; ctx.lineCap = 'round'; var pattern = [5, 5]; if (Array.isArray(this.options.dashes) === true) { pattern = this.options.dashes; } // only firefox and chrome support this method, else we use the legacy one. if (ctx.setLineDash !== undefined) { ctx.save(); // set dash settings for chrome or firefox ctx.setLineDash(pattern); ctx.lineDashOffset = 0; // draw the line if (this.from != this.to) { // draw line via = this._line(ctx); } else { var _getCircleData3 = this._getCircleData(ctx); var _getCircleData32 = _slicedToArray(_getCircleData3, 3); var x = _getCircleData32[0]; var y = _getCircleData32[1]; var radius = _getCircleData32[2]; this._circle(ctx, x, y, radius); } // restore the dash settings. ctx.setLineDash([0]); ctx.lineDashOffset = 0; ctx.restore(); } else { // unsupporting smooth lines if (this.from != this.to) { // draw line ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern); } else { var _getCircleData4 = this._getCircleData(ctx); var _getCircleData42 = _slicedToArray(_getCircleData4, 3); var x = _getCircleData42[0]; var y = _getCircleData42[1]; var radius = _getCircleData42[2]; this._circle(ctx, x, y, radius); } // draw shadow if enabled this.enableShadow(ctx); ctx.stroke(); // disable shadows for other elements. this.disableShadow(ctx); } return via; } }, { key: 'findBorderPosition', value: function findBorderPosition(nearNode, ctx, options) { if (this.from != this.to) { return this._findBorderPosition(nearNode, ctx, options); } else { return this._findBorderPositionCircle(nearNode, ctx, options); } } }, { key: 'findBorderPositions', value: function findBorderPositions(ctx) { var from = {}; var to = {}; if (this.from != this.to) { from = this._findBorderPosition(this.from, ctx); to = this._findBorderPosition(this.to, ctx); } else { var _getCircleData5 = this._getCircleData(ctx); var _getCircleData52 = _slicedToArray(_getCircleData5, 3); var x = _getCircleData52[0]; var y = _getCircleData52[1]; var radius = _getCircleData52[2]; from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); } return { from: from, to: to }; } }, { key: '_getCircleData', value: function _getCircleData(ctx) { var x = undefined, y = undefined; var node = this.from; var radius = this.options.selfReferenceSize; if (ctx !== undefined) { if (node.shape.width === undefined) { node.shape.resize(ctx); } } // get circle coordinates if (node.shape.width > node.shape.height) { x = node.x + node.shape.width * 0.5; y = node.y - radius; } else { x = node.x + radius; y = node.y - node.shape.height * 0.5; } return [x, y, radius]; } }, { key: '_pointOnCircle', /** * Get a point on a circle * @param {Number} x * @param {Number} y * @param {Number} radius * @param {Number} percentage. Value between 0 (line start) and 1 (line end) * @return {Object} point * @private */ value: function _pointOnCircle(x, y, radius, percentage) { var angle = percentage * 2 * Math.PI; return { x: x + radius * Math.cos(angle), y: y - radius * Math.sin(angle) }; } }, { key: '_findBorderPositionCircle', /** * This function uses binary search to look for the point where the circle crosses the border of the node. * @param node * @param ctx * @param options * @returns {*} * @private */ value: function _findBorderPositionCircle(node, ctx, options) { var x = options.x; var y = options.y; var low = options.low; var high = options.high; var direction = options.direction; var maxIterations = 10; var iteration = 0; var radius = this.options.selfReferenceSize; var pos = undefined, angle = undefined, distanceToBorder = undefined, distanceToPoint = undefined, difference = undefined; var threshold = 0.05; var middle = (low + high) * 0.5; while (low <= high && iteration < maxIterations) { middle = (low + high) * 0.5; pos = this._pointOnCircle(x, y, radius, middle); angle = Math.atan2(node.y - pos.y, node.x - pos.x); distanceToBorder = node.distanceToBorder(ctx, angle); distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); difference = distanceToBorder - distanceToPoint; if (Math.abs(difference) < threshold) { break; // found } else if (difference > 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. if (direction > 0) { low = middle; } else { high = middle; } } else { if (direction > 0) { high = middle; } else { low = middle; } } iteration++; } pos.t = middle; return pos; } }, { key: 'getLineWidth', /** * Get the line width of the edge. Depends on width and whether one of the * connected nodes is selected. * @return {Number} width * @private */ value: function getLineWidth(selected, hover) { if (selected === true) { return Math.max(this.selectionWidth, 0.3 / this.body.view.scale); } else { if (hover === true) { return Math.max(this.hoverWidth, 0.3 / this.body.view.scale); } else { return Math.max(this.options.width, 0.3 / this.body.view.scale); } } } }, { key: 'getColor', value: function getColor(ctx, selected, hover) { var colorOptions = this.options.color; if (colorOptions.inherit !== false) { // when this is a loop edge, just use the 'from' method if (colorOptions.inherit === 'both' && this.from.id !== this.to.id) { var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); var fromColor = undefined, toColor = undefined; fromColor = this.from.options.color.highlight.border; toColor = this.to.options.color.highlight.border; if (this.from.selected === false && this.to.selected === false) { fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); } else if (this.from.selected === true && this.to.selected === false) { toColor = this.to.options.color.border; } else if (this.from.selected === false && this.to.selected === true) { fromColor = this.from.options.color.border; } grd.addColorStop(0, fromColor); grd.addColorStop(1, toColor); // -------------------- this returns -------------------- // return grd; } if (this.colorDirty === true) { if (colorOptions.inherit === 'to') { this.color.highlight = this.to.options.color.highlight.border; this.color.hover = this.to.options.color.hover.border; this.color.color = util.overrideOpacity(this.to.options.color.border, colorOptions.opacity); } else { // (this.options.color.inherit.source === "from") { this.color.highlight = this.from.options.color.highlight.border; this.color.hover = this.from.options.color.hover.border; this.color.color = util.overrideOpacity(this.from.options.color.border, colorOptions.opacity); } } } else if (this.colorDirty === true) { this.color.highlight = colorOptions.highlight; this.color.hover = colorOptions.hover; this.color.color = util.overrideOpacity(colorOptions.color, colorOptions.opacity); } // if color inherit is on and gradients are used, the function has already returned by now. this.colorDirty = false; if (selected === true) { return this.color.highlight; } else if (hover === true) { return this.color.hover; } else { return this.color.color; } } }, { key: '_circle', /** * Draw a line from a node to itself, a circle * @param {CanvasRenderingContext2D} ctx * @param {Number} x * @param {Number} y * @param {Number} radius * @private */ value: function _circle(ctx, x, y, radius) { // draw shadow if enabled this.enableShadow(ctx); // draw a circle ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI, false); ctx.stroke(); // disable shadows for other elements. this.disableShadow(ctx); } }, { key: 'getDistanceToEdge', /** * Calculate the distance between a point (x3,y3) and a line segment from * (x1,y1) to (x2,y2). * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @private */ value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { // x3,y3 is the point var returnValue = 0; if (this.from != this.to) { returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); } else { var _getCircleData6 = this._getCircleData(); var _getCircleData62 = _slicedToArray(_getCircleData6, 3); var x = _getCircleData62[0]; var y = _getCircleData62[1]; var radius = _getCircleData62[2]; var dx = x - x3; var dy = y - y3; returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); } if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { return 0; } else { return returnValue; } } }, { key: '_getDistanceToLine', value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { var px = x2 - x1; var py = y2 - y1; var something = px * px + py * py; var u = ((x3 - x1) * px + (y3 - y1) * py) / something; if (u > 1) { u = 1; } else if (u < 0) { u = 0; } var x = x1 + u * px; var y = y1 + u * py; var dx = x - x3; var dy = y - y3; //# Note: If the actual distance does not matter, //# if you only want to compare what this function //# returns to other results of this function, you //# can just return the squared distance instead //# (i.e. remove the sqrt) to gain a little performance return Math.sqrt(dx * dx + dy * dy); } }, { key: 'drawArrowHead', /** * * @param ctx * @param position * @param viaNode */ value: function drawArrowHead(ctx, position, viaNode, selected, hover) { // set style ctx.strokeStyle = this.getColor(ctx, selected, hover); ctx.fillStyle = ctx.strokeStyle; ctx.lineWidth = this.getLineWidth(selected, hover); // set lets var angle = undefined; var length = undefined; var arrowPos = undefined; var node1 = undefined; var node2 = undefined; var guideOffset = undefined; var scaleFactor = undefined; if (position === 'from') { node1 = this.from; node2 = this.to; guideOffset = 0.1; scaleFactor = this.options.arrows.from.scaleFactor; } else if (position === 'to') { node1 = this.to; node2 = this.from; guideOffset = -0.1; scaleFactor = this.options.arrows.to.scaleFactor; } else { node1 = this.to; node2 = this.from; scaleFactor = this.options.arrows.middle.scaleFactor; } // if not connected to itself if (node1 != node2) { if (position !== 'middle') { // draw arrow head if (this.options.smooth.enabled === true) { arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); } else { angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); arrowPos = this.findBorderPosition(node1, ctx); } } else { angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. } // draw arrow at the end of the line length = (10 + 5 * this.options.width) * scaleFactor; ctx.arrow(arrowPos.x, arrowPos.y, angle, length); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); } else { // draw circle var _angle = undefined, point = undefined; var _getCircleData7 = this._getCircleData(ctx); var _getCircleData72 = _slicedToArray(_getCircleData7, 3); var x = _getCircleData72[0]; var y = _getCircleData72[1]; var radius = _getCircleData72[2]; if (position === 'from') { point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; } else if (position === 'to') { point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; } else { point = this._pointOnCircle(x, y, radius, 0.175); _angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; } // draw the arrowhead var _length = (10 + 5 * this.options.width) * scaleFactor; ctx.arrow(point.x, point.y, _angle, _length); // draw shadow if enabled this.enableShadow(ctx); ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); ctx.stroke(); } } }, { key: 'enableShadow', value: function enableShadow(ctx) { if (this.options.shadow.enabled === true) { ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = this.options.shadow.size; ctx.shadowOffsetX = this.options.shadow.x; ctx.shadowOffsetY = this.options.shadow.y; } } }, { key: 'disableShadow', value: function disableShadow(ctx) { if (this.options.shadow.enabled === true) { ctx.shadowColor = 'rgba(0,0,0,0)'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } } }]); return EdgeBase; })(); exports['default'] = EdgeBase; module.exports = exports['default']; /***/ }, /* 86 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilBezierEdgeBase = __webpack_require__(84); var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); var BezierEdgeStatic = (function (_BezierEdgeBase) { function BezierEdgeStatic(options, body, labelModule) { _classCallCheck(this, BezierEdgeStatic); _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(BezierEdgeStatic, _BezierEdgeBase); _createClass(BezierEdgeStatic, [{ key: '_line', /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx * @private */ value: function _line(ctx) { // draw a straight line ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); var via = this._getViaCoordinates(); var returnValue = via; // fallback to normal straight edges if (via.x === undefined) { ctx.lineTo(this.to.x, this.to.y); returnValue = undefined; } else { ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); } // draw shadow if enabled this.enableShadow(ctx); ctx.stroke(); this.disableShadow(ctx); return returnValue; } }, { key: '_getViaCoordinates', value: function _getViaCoordinates() { var xVia = undefined; var yVia = undefined; var factor = this.options.smooth.roundness; var type = this.options.smooth.type; var dx = Math.abs(this.from.x - this.to.x); var dy = Math.abs(this.from.y - this.to.y); if (type === 'discrete' || type === 'diagonalCross') { if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y - factor * dy; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y - factor * dy; } } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y + factor * dy; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y + factor * dy; } } if (type === 'discrete') { xVia = dx < factor * dy ? this.from.x : xVia; } } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y - factor * dx; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y - factor * dx; } } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y + factor * dx; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y + factor * dx; } } if (type === 'discrete') { yVia = dy < factor * dx ? this.from.y : yVia; } } } else if (type === 'straightCross') { if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down xVia = this.from.x; if (this.from.y < this.to.y) { yVia = this.to.y - (1 - factor) * dy; } else { yVia = this.to.y + (1 - factor) * dy; } } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right if (this.from.x < this.to.x) { xVia = this.to.x - (1 - factor) * dx; } else { xVia = this.to.x + (1 - factor) * dx; } yVia = this.from.y; } } else if (type === 'horizontal') { if (this.from.x < this.to.x) { xVia = this.to.x - (1 - factor) * dx; } else { xVia = this.to.x + (1 - factor) * dx; } yVia = this.from.y; } else if (type === 'vertical') { xVia = this.from.x; if (this.from.y < this.to.y) { yVia = this.to.y - (1 - factor) * dy; } else { yVia = this.to.y + (1 - factor) * dy; } } else if (type === 'curvedCW') { dx = this.to.x - this.from.x; dy = this.from.y - this.to.y; var radius = Math.sqrt(dx * dx + dy * dy); var pi = Math.PI; var originalAngle = Math.atan2(dy, dx); var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); } else if (type === 'curvedCCW') { dx = this.to.x - this.from.x; dy = this.from.y - this.to.y; var radius = Math.sqrt(dx * dx + dy * dy); var pi = Math.PI; var originalAngle = Math.atan2(dy, dx); var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); } else { // continuous if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y - factor * dy; xVia = this.to.x < xVia ? this.to.x : xVia; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y - factor * dy; xVia = this.to.x > xVia ? this.to.x : xVia; } } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y + factor * dy; xVia = this.to.x < xVia ? this.to.x : xVia; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y + factor * dy; xVia = this.to.x > xVia ? this.to.x : xVia; } } } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y - factor * dx; yVia = this.to.y > yVia ? this.to.y : yVia; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y - factor * dx; yVia = this.to.y > yVia ? this.to.y : yVia; } } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y + factor * dx; yVia = this.to.y < yVia ? this.to.y : yVia; } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y + factor * dx; yVia = this.to.y < yVia ? this.to.y : yVia; } } } } return { x: xVia, y: yVia }; } }, { key: '_findBorderPosition', value: function _findBorderPosition(nearNode, ctx) { var options = arguments[2] === undefined ? {} : arguments[2]; return this._findBorderPositionBezier(nearNode, ctx, options.via); } }, { key: '_getDistanceToEdge', value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; // x3,y3 is the point return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); } }, { key: 'getPoint', /** * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way * @param percentage * @param via * @returns {{x: number, y: number}} * @private */ value: function getPoint(percentage) { var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; var t = percentage; var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; return { x: x, y: y }; } }]); return BezierEdgeStatic; })(_utilBezierEdgeBase2['default']); exports['default'] = BezierEdgeStatic; module.exports = exports['default']; /***/ }, /* 87 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilEdgeBase = __webpack_require__(85); var _utilEdgeBase2 = _interopRequireDefault(_utilEdgeBase); var StraightEdge = (function (_EdgeBase) { function StraightEdge(options, body, labelModule) { _classCallCheck(this, StraightEdge); _get(Object.getPrototypeOf(StraightEdge.prototype), 'constructor', this).call(this, options, body, labelModule); } _inherits(StraightEdge, _EdgeBase); _createClass(StraightEdge, [{ key: '_line', /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx * @private */ value: function _line(ctx) { // draw a straight line ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); ctx.lineTo(this.to.x, this.to.y); // draw shadow if enabled this.enableShadow(ctx); ctx.stroke(); this.disableShadow(ctx); return undefined; } }, { key: 'getPoint', /** * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way * @param percentage * @param via * @returns {{x: number, y: number}} * @private */ value: function getPoint(percentage) { return { x: (1 - percentage) * this.from.x + percentage * this.to.x, y: (1 - percentage) * this.from.y + percentage * this.to.y }; } }, { key: '_findBorderPosition', value: function _findBorderPosition(nearNode, ctx) { var node1 = this.to; var node2 = this.from; if (nearNode.id === this.from.id) { node1 = this.from; node2 = this.to; } var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); var dx = node1.x - node2.x; var dy = node1.y - node2.y; var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); var toBorderDist = nearNode.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; var borderPos = {}; borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; return borderPos; } }, { key: '_getDistanceToEdge', value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); } }]); return StraightEdge; })(_utilEdgeBase2['default']); exports['default'] = StraightEdge; module.exports = exports['default']; /***/ }, /* 88 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _componentsPhysicsBarnesHutSolver = __webpack_require__(89); var _componentsPhysicsBarnesHutSolver2 = _interopRequireDefault(_componentsPhysicsBarnesHutSolver); var _componentsPhysicsRepulsionSolver = __webpack_require__(90); var _componentsPhysicsRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsRepulsionSolver); var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(91); var _componentsPhysicsHierarchicalRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalRepulsionSolver); var _componentsPhysicsSpringSolver = __webpack_require__(92); var _componentsPhysicsSpringSolver2 = _interopRequireDefault(_componentsPhysicsSpringSolver); var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(93); var _componentsPhysicsHierarchicalSpringSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalSpringSolver); var _componentsPhysicsCentralGravitySolver = __webpack_require__(94); var _componentsPhysicsCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsCentralGravitySolver); var _componentsPhysicsFA2BasedRepulsionSolver = __webpack_require__(95); var _componentsPhysicsFA2BasedRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedRepulsionSolver); var _componentsPhysicsFA2BasedCentralGravitySolver = __webpack_require__(96); var _componentsPhysicsFA2BasedCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedCentralGravitySolver); var util = __webpack_require__(1); var PhysicsEngine = (function () { function PhysicsEngine(body) { _classCallCheck(this, PhysicsEngine); this.body = body; this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; this.physicsEnabled = true; this.simulationInterval = 1000 / 60; this.requiresTimeout = true; this.previousStates = {}; this.freezeCache = {}; this.renderTimer = undefined; this.initialStabilizationEmitted = false; this.stabilized = false; this.startedStabilization = false; this.stabilizationIterations = 0; this.ready = false; // will be set to true if the stabilize // default options this.options = {}; this.defaultOptions = { enabled: true, barnesHut: { theta: 0.5, gravitationalConstant: -2000, centralGravity: 0.3, springLength: 95, springConstant: 0.04, damping: 0.09, avoidOverlap: 0 }, forceAtlas2Based: { theta: 0.5, gravitationalConstant: -50, centralGravity: 0.01, springConstant: 0.08, springLength: 100, damping: 0.4, avoidOverlap: 0 }, repulsion: { centralGravity: 0.2, springLength: 200, springConstant: 0.05, nodeDistance: 100, damping: 0.09, avoidOverlap: 0 }, hierarchicalRepulsion: { centralGravity: 0, springLength: 100, springConstant: 0.01, nodeDistance: 120, damping: 0.09 }, maxVelocity: 50, minVelocity: 0.1, // px/s solver: 'barnesHut', stabilization: { enabled: true, iterations: 1000, // maximum number of iteration to stabilize updateInterval: 50, onlyDynamicEdges: false, fit: true }, timestep: 0.5 }; util.extend(this.options, this.defaultOptions); this.bindEventListeners(); } _createClass(PhysicsEngine, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; this.body.emitter.on('initPhysics', function () { _this.initPhysics(); }); this.body.emitter.on('resetPhysics', function () { _this.stopSimulation();_this.ready = false; }); this.body.emitter.on('disablePhysics', function () { _this.physicsEnabled = false;_this.stopSimulation(); }); this.body.emitter.on('restorePhysics', function () { _this.setOptions(_this.options); if (_this.ready === true) { _this.startSimulation(); } }); this.body.emitter.on('startSimulation', function () { if (_this.ready === true) { _this.startSimulation(); } }); this.body.emitter.on('stopSimulation', function () { _this.stopSimulation(); }); this.body.emitter.on('destroy', function () { _this.stopSimulation(false); _this.body.emitter.off(); }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { if (options === false) { this.options.enabled = false; this.physicsEnabled = false; this.stopSimulation(); } else { this.physicsEnabled = true; util.selectiveNotDeepExtend(['stabilization'], this.options, options); util.mergeOptions(this.options, options, 'stabilization'); if (options.enabled === undefined) { this.options.enabled = true; } if (this.options.enabled === false) { this.physicsEnabled = false; this.stopSimulation(); } } } this.init(); } }, { key: 'init', value: function init() { var options; if (this.options.solver === 'forceAtlas2Based') { options = this.options.forceAtlas2Based; this.nodesSolver = new _componentsPhysicsFA2BasedRepulsionSolver2['default'](this.body, this.physicsBody, options); this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); this.gravitySolver = new _componentsPhysicsFA2BasedCentralGravitySolver2['default'](this.body, this.physicsBody, options); } else if (this.options.solver === 'repulsion') { options = this.options.repulsion; this.nodesSolver = new _componentsPhysicsRepulsionSolver2['default'](this.body, this.physicsBody, options); this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); this.gravitySolver = new _componentsPhysicsCentralGravitySolver2['default'](this.body, this.physicsBody, options); } else if (this.options.solver === 'hierarchicalRepulsion') { options = this.options.hierarchicalRepulsion; this.nodesSolver = new _componentsPhysicsHierarchicalRepulsionSolver2['default'](this.body, this.physicsBody, options); this.edgesSolver = new _componentsPhysicsHierarchicalSpringSolver2['default'](this.body, this.physicsBody, options); this.gravitySolver = new _componentsPhysicsCentralGravitySolver2['default'](this.body, this.physicsBody, options); } else { // barnesHut options = this.options.barnesHut; this.nodesSolver = new _componentsPhysicsBarnesHutSolver2['default'](this.body, this.physicsBody, options); this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); this.gravitySolver = new _componentsPhysicsCentralGravitySolver2['default'](this.body, this.physicsBody, options); } this.modelOptions = options; } }, { key: 'initPhysics', value: function initPhysics() { if (this.physicsEnabled === true && this.options.enabled === true) { if (this.options.stabilization.enabled === true) { this.stabilize(); } else { this.stabilized = false; this.ready = true; this.body.emitter.emit('fit', {}, true); this.startSimulation(); } } else { this.ready = true; this.body.emitter.emit('fit'); } } }, { key: 'startSimulation', /** * Start the simulation */ value: function startSimulation() { if (this.physicsEnabled === true && this.options.enabled === true) { this.stabilized = false; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit('_resizeNodes'); if (this.viewFunction === undefined) { this.viewFunction = this.simulationStep.bind(this); this.body.emitter.on('initRedraw', this.viewFunction); this.body.emitter.emit('_startRendering'); } } else { this.body.emitter.emit('_redraw'); } } }, { key: 'stopSimulation', /** * Stop the simulation, force stabilization. */ value: function stopSimulation() { var emit = arguments[0] === undefined ? true : arguments[0]; this.stabilized = true; if (emit === true) { this._emitStabilized(); } if (this.viewFunction !== undefined) { this.body.emitter.off('initRedraw', this.viewFunction); this.viewFunction = undefined; if (emit === true) { this.body.emitter.emit('_stopRendering'); } } } }, { key: 'simulationStep', /** * The viewFunction inserts this step into each renderloop. It calls the physics tick and handles the cleanup at stabilized. * */ value: function simulationStep() { // check if the physics have settled var startTime = Date.now(); this.physicsTick(); var physicsTime = Date.now() - startTime; // run double speed if it is a little graph if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed === true) && this.stabilized === false) { this.physicsTick(); // this makes sure there is no jitter. The decision is taken once to run it at double speed. this.runDoubleSpeed = true; } if (this.stabilized === true) { if (this.stabilizationIterations > 1) { // trigger the 'stabilized' event. // The event is triggered on the next tick, to prevent the case that // it is fired while initializing the Network, in which case you would not // be able to catch it this.startedStabilization = false; //this._emitStabilized(); } this.stopSimulation(); } } }, { key: '_emitStabilized', value: function _emitStabilized() { var _this2 = this; if (this.stabilizationIterations > 1 || this.initialStabilizationEmitted === false) { this.initialStabilizationEmitted = true; setTimeout(function () { _this2.body.emitter.emit('stabilized', { iterations: _this2.stabilizationIterations }); _this2.stabilizationIterations = 0; }, 0); } } }, { key: 'physicsTick', /** * A single simulation step (or 'tick') in the physics simulation * * @private */ value: function physicsTick() { if (this.stabilized === false) { this.calculateForces(); this.stabilized = this.moveNodes(); // determine if the network has stabilzied if (this.stabilized === true) { this.revert(); } else { // this is here to ensure that there is no start event when the network is already stable. if (this.startedStabilization === false) { this.body.emitter.emit('startStabilizing'); this.startedStabilization = true; } } this.stabilizationIterations++; } } }, { key: 'updatePhysicsData', /** * Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time. * * @private */ value: function updatePhysicsData() { this.physicsBody.forces = {}; this.physicsBody.physicsNodeIndices = []; this.physicsBody.physicsEdgeIndices = []; var nodes = this.body.nodes; var edges = this.body.edges; // get node indices for physics for (var nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { if (nodes[nodeId].options.physics === true) { this.physicsBody.physicsNodeIndices.push(nodeId); } } } // get edge indices for physics for (var edgeId in edges) { if (edges.hasOwnProperty(edgeId)) { if (edges[edgeId].options.physics === true) { this.physicsBody.physicsEdgeIndices.push(edgeId); } } } // get the velocity and the forces vector for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { var nodeId = this.physicsBody.physicsNodeIndices[i]; this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; // forces can be reset because they are recalculated. Velocities have to persist. if (this.physicsBody.velocities[nodeId] === undefined) { this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; } } // clean deleted nodes from the velocity vector for (var nodeId in this.physicsBody.velocities) { if (nodes[nodeId] === undefined) { delete this.physicsBody.velocities[nodeId]; } } } }, { key: 'revert', /** * Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized. */ value: function revert() { var nodeIds = Object.keys(this.previousStates); var nodes = this.body.nodes; var velocities = this.physicsBody.velocities; for (var i = 0; i < nodeIds.length; i++) { var nodeId = nodeIds[i]; if (nodes[nodeId] !== undefined) { if (nodes[nodeId].options.physics === true) { velocities[nodeId].x = this.previousStates[nodeId].vx; velocities[nodeId].y = this.previousStates[nodeId].vy; nodes[nodeId].x = this.previousStates[nodeId].x; nodes[nodeId].y = this.previousStates[nodeId].y; } } else { delete this.previousStates[nodeId]; } } } }, { key: 'moveNodes', /** * move the nodes one timestap and check if they are stabilized * @returns {boolean} */ value: function moveNodes() { var nodesPresent = false; var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1000000000; var stabilized = true; var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; var nodeVelocity = this._performStep(nodeId, maxVelocity); // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized stabilized = nodeVelocity < vminCorrected && stabilized === true; nodesPresent = true; } if (nodesPresent === true) { if (vminCorrected > 0.5 * this.options.maxVelocity) { return false; } else { return stabilized; } } return true; } }, { key: '_performStep', /** * Perform the actual step * * @param nodeId * @param maxVelocity * @returns {number} * @private */ value: function _performStep(nodeId, maxVelocity) { var node = this.body.nodes[nodeId]; var timestep = this.options.timestep; var forces = this.physicsBody.forces; var velocities = this.physicsBody.velocities; // store the state so we can revert this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; if (node.options.fixed.x === false) { var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration velocities[nodeId].x += ax * timestep; // velocity velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; node.x += velocities[nodeId].x * timestep; // position } else { forces[nodeId].x = 0; velocities[nodeId].x = 0; } if (node.options.fixed.y === false) { var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration velocities[nodeId].y += ay * timestep; // velocity velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; node.y += velocities[nodeId].y * timestep; // position } else { forces[nodeId].y = 0; velocities[nodeId].y = 0; } var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); return totalVelocity; } }, { key: 'calculateForces', /** * calculate the forces for one physics iteration. */ value: function calculateForces() { this.gravitySolver.solve(); this.nodesSolver.solve(); this.edgesSolver.solve(); } }, { key: '_freezeNodes', /** * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization * because only the supportnodes for the smoothCurves have to settle. * * @private */ value: function _freezeNodes() { var nodes = this.body.nodes; for (var id in nodes) { if (nodes.hasOwnProperty(id)) { if (nodes[id].x && nodes[id].y) { this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; nodes[id].options.fixed.x = true; nodes[id].options.fixed.y = true; } } } } }, { key: '_restoreFrozenNodes', /** * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. * * @private */ value: function _restoreFrozenNodes() { var nodes = this.body.nodes; for (var id in nodes) { if (nodes.hasOwnProperty(id)) { if (this.freezeCache[id] !== undefined) { nodes[id].options.fixed.x = this.freezeCache[id].x; nodes[id].options.fixed.y = this.freezeCache[id].y; } } } this.freezeCache = {}; } }, { key: 'stabilize', /** * Find a stable position for all nodes * @private */ value: function stabilize() { var _this3 = this; var iterations = arguments[0] === undefined ? this.options.stabilization.iterations : arguments[0]; if (typeof iterations !== 'number') { console.log('The stabilize method needs a numeric amount of iterations. Switching to default: ', this.options.stabilization.iterations); iterations = this.options.stabilization.iterations; } // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit('_resizeNodes'); // stop the render loop this.stopSimulation(); // set stabilze to false this.stabilized = false; // block redraw requests this.body.emitter.emit('_blockRedrawRequests'); this.targetIterations = iterations; // start the stabilization if (this.options.stabilization.onlyDynamicEdges === true) { this._freezeNodes(); } this.stabilizationIterations = 0; setTimeout(function () { return _this3._stabilizationBatch(); }, 0); } }, { key: '_stabilizationBatch', value: function _stabilizationBatch() { var count = 0; while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) { this.physicsTick(); this.stabilizationIterations++; count++; } if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) { this.body.emitter.emit('stabilizationProgress', { iterations: this.stabilizationIterations, total: this.targetIterations }); setTimeout(this._stabilizationBatch.bind(this), 0); } else { this._finalizeStabilization(); } } }, { key: '_finalizeStabilization', value: function _finalizeStabilization() { this.body.emitter.emit('_allowRedrawRequests'); if (this.options.stabilization.fit === true) { this.body.emitter.emit('fit'); } if (this.options.stabilization.onlyDynamicEdges === true) { this._restoreFrozenNodes(); } this.body.emitter.emit('stabilizationIterationsDone'); this.body.emitter.emit('_requestRedraw'); if (this.stabilized === true) { this._emitStabilized(); } else { this.startSimulation(); } this.ready = true; } }]); return PhysicsEngine; })(); exports['default'] = PhysicsEngine; module.exports = exports['default']; /***/ }, /* 89 */ /***/ function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var BarnesHutSolver = (function () { function BarnesHutSolver(body, physicsBody, options) { _classCallCheck(this, BarnesHutSolver); this.body = body; this.physicsBody = physicsBody; this.barnesHutTree; this.setOptions(options); this.randomSeed = 5; } _createClass(BarnesHutSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; this.thetaInversed = 1 / this.options.theta; this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1, this.options.avoidOverlap)); // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius } }, { key: "seededRandom", value: function seededRandom() { var x = Math.sin(this.randomSeed++) * 10000; return x - Math.floor(x); } }, { key: "solve", /** * This function calculates the forces the nodes apply on eachother based on a gravitational model. * The Barnes Hut method is used to speed up this N-body simulation. * * @private */ value: function solve() { if (this.options.gravitationalConstant !== 0 && this.physicsBody.physicsNodeIndices.length > 0) { var node = undefined; var nodes = this.body.nodes; var nodeIndices = this.physicsBody.physicsNodeIndices; var nodeCount = nodeIndices.length; // create the tree var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); // for debugging this.barnesHutTree = barnesHutTree; // place the nodes one by one recursively for (var i = 0; i < nodeCount; i++) { node = nodes[nodeIndices[i]]; if (node.options.mass > 0) { // starting with root is irrelevant, it never passes the BarnesHutSolver condition this._getForceContribution(barnesHutTree.root.children.NW, node); this._getForceContribution(barnesHutTree.root.children.NE, node); this._getForceContribution(barnesHutTree.root.children.SW, node); this._getForceContribution(barnesHutTree.root.children.SE, node); } } } } }, { key: "_getForceContribution", /** * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. * If a region contains a single node, we check if it is not itself, then we apply the force. * * @param parentBranch * @param node * @private */ value: function _getForceContribution(parentBranch, node) { // we get no force contribution from an empty region if (parentBranch.childrenCount > 0) { var dx = undefined, dy = undefined, distance = undefined; // get the distance from the center of mass to the node. dx = parentBranch.centerOfMass.x - node.x; dy = parentBranch.centerOfMass.y - node.y; distance = Math.sqrt(dx * dx + dy * dy); // BarnesHutSolver condition // original condition : s/d < theta = passed === d/s > 1/theta = passed // calcSize = 1/s --> d * 1/s > 1/theta = passed if (distance * parentBranch.calcSize > this.thetaInversed) { this._calculateForces(distance, dx, dy, node, parentBranch); } else { // Did not pass the condition, go into children if available if (parentBranch.childrenCount === 4) { this._getForceContribution(parentBranch.children.NW, node); this._getForceContribution(parentBranch.children.NE, node); this._getForceContribution(parentBranch.children.SW, node); this._getForceContribution(parentBranch.children.SE, node); } else { // parentBranch must have only one node, if it was empty we wouldnt be here if (parentBranch.children.data.id != node.id) { // if it is not self this._calculateForces(distance, dx, dy, node, parentBranch); } } } } } }, { key: "_calculateForces", /** * Calculate the forces based on the distance. * * @param distance * @param dx * @param dy * @param node * @param parentBranch * @private */ value: function _calculateForces(distance, dx, dy, node, parentBranch) { if (distance === 0) { distance = 0.1; dx = distance; } if (this.overlapAvoidanceFactor < 1) { distance = Math.max(0.1 + this.overlapAvoidanceFactor * node.shape.radius, distance - node.shape.radius); } // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / Math.pow(distance, 3); var fx = dx * gravityForce; var fy = dy * gravityForce; this.physicsBody.forces[node.id].x += fx; this.physicsBody.forces[node.id].y += fy; } }, { key: "_formBarnesHutTree", /** * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. * * @param nodes * @param nodeIndices * @private */ value: function _formBarnesHutTree(nodes, nodeIndices) { var node = undefined; var nodeCount = nodeIndices.length; var minX = nodes[nodeIndices[0]].x; var minY = nodes[nodeIndices[0]].y; var maxX = nodes[nodeIndices[0]].x; var maxY = nodes[nodeIndices[0]].y; // get the range of the nodes for (var i = 1; i < nodeCount; i++) { var x = nodes[nodeIndices[i]].x; var y = nodes[nodeIndices[i]].y; if (nodes[nodeIndices[i]].options.mass > 0) { if (x < minX) { minX = x; } if (x > maxX) { maxX = x; } if (y < minY) { minY = y; } if (y > maxY) { maxY = y; } } } // make the range a square var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y if (sizeDiff > 0) { minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff; } // xSize > ySize else { minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff; } // xSize < ySize var minimumTreeSize = 0.00001; var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); var halfRootSize = 0.5 * rootSize; var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); // construct the barnesHutTree var barnesHutTree = { root: { centerOfMass: { x: 0, y: 0 }, mass: 0, range: { minX: centerX - halfRootSize, maxX: centerX + halfRootSize, minY: centerY - halfRootSize, maxY: centerY + halfRootSize }, size: rootSize, calcSize: 1 / rootSize, children: { data: null }, maxWidth: 0, level: 0, childrenCount: 4 } }; this._splitBranch(barnesHutTree.root); // place the nodes one by one recursively for (var i = 0; i < nodeCount; i++) { node = nodes[nodeIndices[i]]; if (node.options.mass > 0) { this._placeInTree(barnesHutTree.root, node); } } // make global return barnesHutTree; } }, { key: "_updateBranchMass", /** * this updates the mass of a branch. this is increased by adding a node. * * @param parentBranch * @param node * @private */ value: function _updateBranchMass(parentBranch, node) { var totalMass = parentBranch.mass + node.options.mass; var totalMassInv = 1 / totalMass; parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; parentBranch.centerOfMass.x *= totalMassInv; parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; parentBranch.centerOfMass.y *= totalMassInv; parentBranch.mass = totalMass; var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; } }, { key: "_placeInTree", /** * determine in which branch the node will be placed. * * @param parentBranch * @param node * @param skipMassUpdate * @private */ value: function _placeInTree(parentBranch, node, skipMassUpdate) { if (skipMassUpdate != true || skipMassUpdate === undefined) { // update the mass of the branch. this._updateBranchMass(parentBranch, node); } if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW if (parentBranch.children.NW.range.maxY > node.y) { // in NW this._placeInRegion(parentBranch, node, "NW"); } else { // in SW this._placeInRegion(parentBranch, node, "SW"); } } else { // in NE or SE if (parentBranch.children.NW.range.maxY > node.y) { // in NE this._placeInRegion(parentBranch, node, "NE"); } else { // in SE this._placeInRegion(parentBranch, node, "SE"); } } } }, { key: "_placeInRegion", /** * actually place the node in a region (or branch) * * @param parentBranch * @param node * @param region * @private */ value: function _placeInRegion(parentBranch, node, region) { switch (parentBranch.children[region].childrenCount) { case 0: // place node here parentBranch.children[region].children.data = node; parentBranch.children[region].childrenCount = 1; this._updateBranchMass(parentBranch.children[region], node); break; case 1: // convert into children // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) // we move one node a pixel and we do not put it in the tree. if (parentBranch.children[region].children.data.x === node.x && parentBranch.children[region].children.data.y === node.y) { node.x += this.seededRandom(); node.y += this.seededRandom(); } else { this._splitBranch(parentBranch.children[region]); this._placeInTree(parentBranch.children[region], node); } break; case 4: // place in branch this._placeInTree(parentBranch.children[region], node); break; } } }, { key: "_splitBranch", /** * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch * after the split is complete. * * @param parentBranch * @private */ value: function _splitBranch(parentBranch) { // if the branch is shaded with a node, replace the node in the new subset. var containedNode = null; if (parentBranch.childrenCount === 1) { containedNode = parentBranch.children.data; parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; } parentBranch.childrenCount = 4; parentBranch.children.data = null; this._insertRegion(parentBranch, "NW"); this._insertRegion(parentBranch, "NE"); this._insertRegion(parentBranch, "SW"); this._insertRegion(parentBranch, "SE"); if (containedNode != null) { this._placeInTree(parentBranch, containedNode); } } }, { key: "_insertRegion", /** * This function subdivides the region into four new segments. * Specifically, this inserts a single new segment. * It fills the children section of the parentBranch * * @param parentBranch * @param region * @param parentRange * @private */ value: function _insertRegion(parentBranch, region) { var minX = undefined, maxX = undefined, minY = undefined, maxY = undefined; var childSize = 0.5 * parentBranch.size; switch (region) { case "NW": minX = parentBranch.range.minX; maxX = parentBranch.range.minX + childSize; minY = parentBranch.range.minY; maxY = parentBranch.range.minY + childSize; break; case "NE": minX = parentBranch.range.minX + childSize; maxX = parentBranch.range.maxX; minY = parentBranch.range.minY; maxY = parentBranch.range.minY + childSize; break; case "SW": minX = parentBranch.range.minX; maxX = parentBranch.range.minX + childSize; minY = parentBranch.range.minY + childSize; maxY = parentBranch.range.maxY; break; case "SE": minX = parentBranch.range.minX + childSize; maxX = parentBranch.range.maxX; minY = parentBranch.range.minY + childSize; maxY = parentBranch.range.maxY; break; } parentBranch.children[region] = { centerOfMass: { x: 0, y: 0 }, mass: 0, range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, size: 0.5 * parentBranch.size, calcSize: 2 * parentBranch.calcSize, children: { data: null }, maxWidth: 0, level: parentBranch.level + 1, childrenCount: 0 }; } }, { key: "_debug", //--------------------------- DEBUGGING BELOW ---------------------------// /** * This function is for debugging purposed, it draws the tree. * * @param ctx * @param color * @private */ value: function _debug(ctx, color) { if (this.barnesHutTree !== undefined) { ctx.lineWidth = 1; this._drawBranch(this.barnesHutTree.root, ctx, color); } } }, { key: "_drawBranch", /** * This function is for debugging purposes. It draws the branches recursively. * * @param branch * @param ctx * @param color * @private */ value: function _drawBranch(branch, ctx, color) { if (color === undefined) { color = "#FF0000"; } if (branch.childrenCount === 4) { this._drawBranch(branch.children.NW, ctx); this._drawBranch(branch.children.NE, ctx); this._drawBranch(branch.children.SE, ctx); this._drawBranch(branch.children.SW, ctx); } ctx.strokeStyle = color; ctx.beginPath(); ctx.moveTo(branch.range.minX, branch.range.minY); ctx.lineTo(branch.range.maxX, branch.range.minY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(branch.range.maxX, branch.range.minY); ctx.lineTo(branch.range.maxX, branch.range.maxY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(branch.range.maxX, branch.range.maxY); ctx.lineTo(branch.range.minX, branch.range.maxY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(branch.range.minX, branch.range.maxY); ctx.lineTo(branch.range.minX, branch.range.minY); ctx.stroke(); /* if (branch.mass > 0) { ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); ctx.stroke(); } */ } }]); return BarnesHutSolver; })(); exports["default"] = BarnesHutSolver; module.exports = exports["default"]; /***/ }, /* 90 */ /***/ function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var RepulsionSolver = (function () { function RepulsionSolver(body, physicsBody, options) { _classCallCheck(this, RepulsionSolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } _createClass(RepulsionSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; } }, { key: "solve", /** * Calculate the forces the nodes apply on each other based on a repulsion field. * This field is linearly approximated. * * @private */ value: function solve() { var dx, dy, distance, fx, fy, repulsingForce, node1, node2; var nodes = this.body.nodes; var nodeIndices = this.physicsBody.physicsNodeIndices; var forces = this.physicsBody.forces; // repulsing forces between nodes var nodeDistance = this.options.nodeDistance; // approximation constants var a = -2 / 3 / nodeDistance; var b = 4 / 3; // we loop from i over all but the last entree in the array // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j for (var i = 0; i < nodeIndices.length - 1; i++) { node1 = nodes[nodeIndices[i]]; for (var j = i + 1; j < nodeIndices.length; j++) { node2 = nodes[nodeIndices[j]]; dx = node2.x - node1.x; dy = node2.y - node1.y; distance = Math.sqrt(dx * dx + dy * dy); // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. if (distance === 0) { distance = 0.1 * Math.random(); dx = distance; } if (distance < 2 * nodeDistance) { if (distance < 0.5 * nodeDistance) { repulsingForce = 1; } else { repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) } repulsingForce = repulsingForce / distance; fx = dx * repulsingForce; fy = dy * repulsingForce; forces[node1.id].x -= fx; forces[node1.id].y -= fy; forces[node2.id].x += fx; forces[node2.id].y += fy; } } } } }]); return RepulsionSolver; })(); exports["default"] = RepulsionSolver; module.exports = exports["default"]; /***/ }, /* 91 */ /***/ function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var HierarchicalRepulsionSolver = (function () { function HierarchicalRepulsionSolver(body, physicsBody, options) { _classCallCheck(this, HierarchicalRepulsionSolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } _createClass(HierarchicalRepulsionSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; } }, { key: "solve", /** * Calculate the forces the nodes apply on each other based on a repulsion field. * This field is linearly approximated. * * @private */ value: function solve() { var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; var nodes = this.body.nodes; var nodeIndices = this.physicsBody.physicsNodeIndices; var forces = this.physicsBody.forces; // repulsing forces between nodes var nodeDistance = this.options.nodeDistance; // we loop from i over all but the last entree in the array // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j for (i = 0; i < nodeIndices.length - 1; i++) { node1 = nodes[nodeIndices[i]]; for (j = i + 1; j < nodeIndices.length; j++) { node2 = nodes[nodeIndices[j]]; // nodes only affect nodes on their level if (node1.level === node2.level) { dx = node2.x - node1.x; dy = node2.y - node1.y; distance = Math.sqrt(dx * dx + dy * dy); var steepness = 0.05; if (distance < nodeDistance) { repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); } else { repulsingForce = 0; } // normalize force with if (distance === 0) { distance = 0.01; } else { repulsingForce = repulsingForce / distance; } fx = dx * repulsingForce; fy = dy * repulsingForce; forces[node1.id].x -= fx; forces[node1.id].y -= fy; forces[node2.id].x += fx; forces[node2.id].y += fy; } } } } }]); return HierarchicalRepulsionSolver; })(); exports["default"] = HierarchicalRepulsionSolver; module.exports = exports["default"]; /***/ }, /* 92 */ /***/ function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var SpringSolver = (function () { function SpringSolver(body, physicsBody, options) { _classCallCheck(this, SpringSolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } _createClass(SpringSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; } }, { key: "solve", /** * This function calculates the springforces on the nodes, accounting for the support nodes. * * @private */ value: function solve() { var edgeLength = undefined, edge = undefined; var edgeIndices = this.physicsBody.physicsEdgeIndices; var edges = this.body.edges; var node1 = undefined, node2 = undefined, node3 = undefined; // forces caused by the edges, modelled as springs for (var i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; if (edge.connected === true && edge.toId !== edge.fromId) { // only calculate forces if nodes are in the same sector if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { if (edge.edgeType.via !== undefined) { edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; node1 = edge.to; node2 = edge.edgeType.via; node3 = edge.from; this._calculateSpringForce(node1, node2, 0.5 * edgeLength); this._calculateSpringForce(node2, node3, 0.5 * edgeLength); } else { // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; this._calculateSpringForce(edge.from, edge.to, edgeLength); } } } } } }, { key: "_calculateSpringForce", /** * This is the code actually performing the calculation for the function above. * * @param node1 * @param node2 * @param edgeLength * @private */ value: function _calculateSpringForce(node1, node2, edgeLength) { var dx = node1.x - node2.x; var dy = node1.y - node2.y; var distance = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01); // the 1/distance is so the fx and fy can be calculated without sine or cosine. var springForce = this.options.springConstant * (edgeLength - distance) / distance; var fx = dx * springForce; var fy = dy * springForce; // handle the case where one node is not part of the physcis if (this.physicsBody.forces[node1.id] !== undefined) { this.physicsBody.forces[node1.id].x += fx; this.physicsBody.forces[node1.id].y += fy; } if (this.physicsBody.forces[node2.id] !== undefined) { this.physicsBody.forces[node2.id].x -= fx; this.physicsBody.forces[node2.id].y -= fy; } } }]); return SpringSolver; })(); exports["default"] = SpringSolver; module.exports = exports["default"]; /***/ }, /* 93 */ /***/ function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var HierarchicalSpringSolver = (function () { function HierarchicalSpringSolver(body, physicsBody, options) { _classCallCheck(this, HierarchicalSpringSolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } _createClass(HierarchicalSpringSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; } }, { key: "solve", /** * This function calculates the springforces on the nodes, accounting for the support nodes. * * @private */ value: function solve() { var edgeLength, edge; var dx, dy, fx, fy, springForce, distance; var edges = this.body.edges; var factor = 0.5; var edgeIndices = this.physicsBody.physicsEdgeIndices; var nodeIndices = this.physicsBody.physicsNodeIndices; var forces = this.physicsBody.forces; // initialize the spring force counters for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; forces[nodeId].springFx = 0; forces[nodeId].springFy = 0; } // forces caused by the edges, modelled as springs for (var i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; if (edge.connected === true) { edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; dx = edge.from.x - edge.to.x; dy = edge.from.y - edge.to.y; distance = Math.sqrt(dx * dx + dy * dy); distance = distance === 0 ? 0.01 : distance; // the 1/distance is so the fx and fy can be calculated without sine or cosine. springForce = this.options.springConstant * (edgeLength - distance) / distance; fx = dx * springForce; fy = dy * springForce; if (edge.to.level != edge.from.level) { if (forces[edge.toId] !== undefined) { forces[edge.toId].springFx -= fx; forces[edge.toId].springFy -= fy; } if (forces[edge.fromId] !== undefined) { forces[edge.fromId].springFx += fx; forces[edge.fromId].springFy += fy; } } else { if (forces[edge.toId] !== undefined) { forces[edge.toId].x -= factor * fx; forces[edge.toId].y -= factor * fy; } if (forces[edge.fromId] !== undefined) { forces[edge.fromId].x += factor * fx; forces[edge.fromId].y += factor * fy; } } } } // normalize spring forces var springForce = 1; var springFx, springFy; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); forces[nodeId].x += springFx; forces[nodeId].y += springFy; } // retain energy balance var totalFx = 0; var totalFy = 0; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; totalFx += forces[nodeId].x; totalFy += forces[nodeId].y; } var correctionFx = totalFx / nodeIndices.length; var correctionFy = totalFy / nodeIndices.length; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; forces[nodeId].x -= correctionFx; forces[nodeId].y -= correctionFy; } } }]); return HierarchicalSpringSolver; })(); exports["default"] = HierarchicalSpringSolver; module.exports = exports["default"]; /***/ }, /* 94 */ /***/ function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var CentralGravitySolver = (function () { function CentralGravitySolver(body, physicsBody, options) { _classCallCheck(this, CentralGravitySolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } _createClass(CentralGravitySolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; } }, { key: "solve", value: function solve() { var dx = undefined, dy = undefined, distance = undefined, node = undefined; var nodes = this.body.nodes; var nodeIndices = this.physicsBody.physicsNodeIndices; var forces = this.physicsBody.forces; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; node = nodes[nodeId]; dx = -node.x; dy = -node.y; distance = Math.sqrt(dx * dx + dy * dy); this._calculateForces(distance, dx, dy, forces, node); } } }, { key: "_calculateForces", /** * Calculate the forces based on the distance. * @private */ value: function _calculateForces(distance, dx, dy, forces, node) { var gravityForce = distance === 0 ? 0 : this.options.centralGravity / distance; forces[node.id].x = dx * gravityForce; forces[node.id].y = dy * gravityForce; } }]); return CentralGravitySolver; })(); exports["default"] = CentralGravitySolver; module.exports = exports["default"]; /***/ }, /* 95 */ /***/ function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _BarnesHutSolver2 = __webpack_require__(89); var _BarnesHutSolver3 = _interopRequireDefault(_BarnesHutSolver2); var ForceAtlas2BasedRepulsionSolver = (function (_BarnesHutSolver) { function ForceAtlas2BasedRepulsionSolver(body, physicsBody, options) { _classCallCheck(this, ForceAtlas2BasedRepulsionSolver); _get(Object.getPrototypeOf(ForceAtlas2BasedRepulsionSolver.prototype), "constructor", this).call(this, body, physicsBody, options); } _inherits(ForceAtlas2BasedRepulsionSolver, _BarnesHutSolver); _createClass(ForceAtlas2BasedRepulsionSolver, [{ key: "_calculateForces", /** * Calculate the forces based on the distance. * * @param distance * @param dx * @param dy * @param node * @param parentBranch * @private */ value: function _calculateForces(distance, dx, dy, node, parentBranch) { if (distance === 0) { distance = 0.1 * Math.random(); dx = distance; } if (this.overlapAvoidanceFactor < 1) { distance = Math.max(0.1 + this.overlapAvoidanceFactor * node.shape.radius, distance - node.shape.radius); } var degree = node.edges.length + 1; // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass * degree / Math.pow(distance, 2); var fx = dx * gravityForce; var fy = dy * gravityForce; this.physicsBody.forces[node.id].x += fx; this.physicsBody.forces[node.id].y += fy; } }]); return ForceAtlas2BasedRepulsionSolver; })(_BarnesHutSolver3["default"]); exports["default"] = ForceAtlas2BasedRepulsionSolver; module.exports = exports["default"]; /***/ }, /* 96 */ /***/ function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _CentralGravitySolver2 = __webpack_require__(94); var _CentralGravitySolver3 = _interopRequireDefault(_CentralGravitySolver2); var ForceAtlas2BasedCentralGravitySolver = (function (_CentralGravitySolver) { function ForceAtlas2BasedCentralGravitySolver(body, physicsBody, options) { _classCallCheck(this, ForceAtlas2BasedCentralGravitySolver); _get(Object.getPrototypeOf(ForceAtlas2BasedCentralGravitySolver.prototype), "constructor", this).call(this, body, physicsBody, options); } _inherits(ForceAtlas2BasedCentralGravitySolver, _CentralGravitySolver); _createClass(ForceAtlas2BasedCentralGravitySolver, [{ key: "_calculateForces", /** * Calculate the forces based on the distance. * @private */ value: function _calculateForces(distance, dx, dy, forces, node) { if (distance > 0) { var degree = node.edges.length + 1; var gravityForce = this.options.centralGravity * degree * node.options.mass; forces[node.id].x = dx * gravityForce; forces[node.id].y = dy * gravityForce; } } }]); return ForceAtlas2BasedCentralGravitySolver; })(_CentralGravitySolver3["default"]); exports["default"] = ForceAtlas2BasedCentralGravitySolver; module.exports = exports["default"]; /***/ }, /* 97 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _componentsNodesCluster = __webpack_require__(98); var _componentsNodesCluster2 = _interopRequireDefault(_componentsNodesCluster); var util = __webpack_require__(1); var ClusterEngine = (function () { function ClusterEngine(body) { var _this = this; _classCallCheck(this, ClusterEngine); this.body = body; this.clusteredNodes = {}; this.options = {}; this.defaultOptions = {}; util.extend(this.options, this.defaultOptions); this.body.emitter.on('_resetData', function () { _this.clusteredNodes = {}; }); } _createClass(ClusterEngine, [{ key: 'setOptions', value: function setOptions(options) { if (options !== undefined) {} } }, { key: 'clusterByHubsize', /** * * @param hubsize * @param options */ value: function clusterByHubsize(hubsize, options) { if (hubsize === undefined) { hubsize = this._getHubSize(); } else if (typeof hubsize === 'object') { options = this._checkOptions(hubsize); hubsize = this._getHubSize(); } var nodesToCluster = []; for (var i = 0; i < this.body.nodeIndices.length; i++) { var node = this.body.nodes[this.body.nodeIndices[i]]; if (node.edges.length >= hubsize) { nodesToCluster.push(node.id); } } for (var i = 0; i < nodesToCluster.length; i++) { this.clusterByConnection(nodesToCluster[i], options, false); } this.body.emitter.emit('_dataChanged'); } }, { key: 'cluster', /** * loop over all nodes, check if they adhere to the condition and cluster if needed. * @param options * @param refreshData */ value: function cluster() { var options = arguments[0] === undefined ? {} : arguments[0]; var refreshData = arguments[1] === undefined ? true : arguments[1]; if (options.joinCondition === undefined) { throw new Error('Cannot call clusterByNodeData without a joinCondition function in the options.'); } // check if the options object is fine, append if needed options = this._checkOptions(options); var childNodesObj = {}; var childEdgesObj = {}; // collect the nodes that will be in the cluster for (var i = 0; i < this.body.nodeIndices.length; i++) { var nodeId = this.body.nodeIndices[i]; var node = this.body.nodes[nodeId]; var clonedOptions = this._cloneOptions(node); if (options.joinCondition(clonedOptions) === true) { childNodesObj[nodeId] = this.body.nodes[nodeId]; // collect the nodes that will be in the cluster for (var _i = 0; _i < node.edges.length; _i++) { var edge = node.edges[_i]; childEdgesObj[edge.id] = edge; } } } this._cluster(childNodesObj, childEdgesObj, options, refreshData); } }, { key: 'clusterOutliers', /** * Cluster all nodes in the network that have only 1 edge * @param options * @param refreshData */ value: function clusterOutliers(options) { var refreshData = arguments[1] === undefined ? true : arguments[1]; options = this._checkOptions(options); var clusters = []; // collect the nodes that will be in the cluster for (var i = 0; i < this.body.nodeIndices.length; i++) { var childNodesObj = {}; var childEdgesObj = {}; var nodeId = this.body.nodeIndices[i]; var visibleEdges = 0; var edge = undefined; for (var j = 0; j < this.body.nodes[nodeId].edges.length; j++) { if (this.body.nodes[nodeId].edges[j].options.hidden === false) { visibleEdges++; edge = this.body.nodes[nodeId].edges[j]; } } if (visibleEdges === 1) { // this is an outlier var childNodeId = this._getConnectedId(edge, nodeId); if (childNodeId !== nodeId) { if (options.joinCondition === undefined) { if (this._checkIfUsed(clusters, nodeId, edge.id) === false && this._checkIfUsed(clusters, childNodeId, edge.id) === false) { childEdgesObj[edge.id] = edge; childNodesObj[nodeId] = this.body.nodes[nodeId]; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } } else { var clonedOptions = this._cloneOptions(this.body.nodes[nodeId]); if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters, nodeId, edge.id) === false) { childEdgesObj[edge.id] = edge; childNodesObj[nodeId] = this.body.nodes[nodeId]; } clonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters, nodeId, edge.id) === false) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } } if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0) { clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); } } } } for (var i = 0; i < clusters.length; i++) { this._cluster(clusters[i].nodes, clusters[i].edges, options, false); } if (refreshData === true) { this.body.emitter.emit('_dataChanged'); } } }, { key: '_checkIfUsed', value: function _checkIfUsed(clusters, nodeId, edgeId) { for (var i = 0; i < clusters.length; i++) { var cluster = clusters[i]; if (cluster.nodes[nodeId] !== undefined || cluster.edges[edgeId] !== undefined) { return true; } } return false; } }, { key: 'clusterByConnection', /** * suck all connected nodes of a node into the node. * @param nodeId * @param options * @param refreshData */ value: function clusterByConnection(nodeId, options) { var refreshData = arguments[2] === undefined ? true : arguments[2]; // kill conditions if (nodeId === undefined) { throw new Error('No nodeId supplied to clusterByConnection!'); } if (this.body.nodes[nodeId] === undefined) { throw new Error('The nodeId given to clusterByConnection does not exist!'); } var node = this.body.nodes[nodeId]; options = this._checkOptions(options, node); if (options.clusterNodeProperties.x === undefined) { options.clusterNodeProperties.x = node.x; } if (options.clusterNodeProperties.y === undefined) { options.clusterNodeProperties.y = node.y; } if (options.clusterNodeProperties.fixed === undefined) { options.clusterNodeProperties.fixed = {}; options.clusterNodeProperties.fixed.x = node.options.fixed.x; options.clusterNodeProperties.fixed.y = node.options.fixed.y; } var childNodesObj = {}; var childEdgesObj = {}; var parentNodeId = node.id; var parentClonedOptions = this._cloneOptions(node); childNodesObj[parentNodeId] = node; // collect the nodes that will be in the cluster for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; var childNodeId = this._getConnectedId(edge, parentNodeId); if (childNodeId !== parentNodeId) { if (options.joinCondition === undefined) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } else { // clone the options and insert some additional parameters that could be interesting. var childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } } } else { childEdgesObj[edge.id] = edge; } } this._cluster(childNodesObj, childEdgesObj, options, refreshData); } }, { key: '_cloneOptions', /** * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. * @param objId * @param type * @returns {{}} * @private */ value: function _cloneOptions(item, type) { var clonedOptions = {}; if (type === undefined || type === 'node') { util.deepExtend(clonedOptions, item.options, true); clonedOptions.x = item.x; clonedOptions.y = item.y; clonedOptions.amountOfConnections = item.edges.length; } else { util.deepExtend(clonedOptions, item.options, true); } return clonedOptions; } }, { key: '_createClusterEdges', /** * This function creates the edges that will be attached to the cluster. * * @param childNodesObj * @param childEdgesObj * @param newEdges * @param options * @private */ value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, clusterEdgeProperties) { var edge = undefined, childNodeId = undefined, childNode = undefined, toId = undefined, fromId = undefined, otherNodeId = undefined; var childKeys = Object.keys(childNodesObj); for (var i = 0; i < childKeys.length; i++) { childNodeId = childKeys[i]; childNode = childNodesObj[childNodeId]; // construct new edges from the cluster to others for (var j = 0; j < childNode.edges.length; j++) { edge = childNode.edges[j]; childEdgesObj[edge.id] = edge; // childNodeId position will be replaced by the cluster. if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here. toId = clusterNodeProperties.id; fromId = edge.fromId; otherNodeId = fromId; } else { toId = edge.toId; fromId = clusterNodeProperties.id; otherNodeId = toId; } // if the node connected to the cluster is also in the cluster we do not need a new edge. if (childNodesObj[otherNodeId] === undefined) { var clonedOptions = this._cloneOptions(edge, 'edge'); util.deepExtend(clonedOptions, clusterEdgeProperties); clonedOptions.from = fromId; clonedOptions.to = toId; clonedOptions.id = 'clusterEdge:' + util.randomUUID(); newEdges.push(this.body.functions.createEdge(clonedOptions)); } } } } }, { key: '_checkOptions', /** * This function checks the options that can be supplied to the different cluster functions * for certain fields and inserts defaults if needed * @param options * @returns {*} * @private */ value: function _checkOptions() { var options = arguments[0] === undefined ? {} : arguments[0]; if (options.clusterEdgeProperties === undefined) { options.clusterEdgeProperties = {}; } if (options.clusterNodeProperties === undefined) { options.clusterNodeProperties = {}; } return options; } }, { key: '_cluster', /** * * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node * @param {Object} childEdgesObj | object with edge objects, id as keys * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} * @param {Boolean} refreshData | when true, do not wrap up * @private */ value: function _cluster(childNodesObj, childEdgesObj, options) { var refreshData = arguments[3] === undefined ? true : arguments[3]; // kill condition: no children so cant cluster if (Object.keys(childNodesObj).length === 0) { return; } var clusterNodeProperties = util.deepExtend({}, options.clusterNodeProperties); // construct the clusterNodeProperties if (options.processProperties !== undefined) { // get the childNode options var childNodesOptions = []; for (var nodeId in childNodesObj) { var clonedOptions = this._cloneOptions(childNodesObj[nodeId]); childNodesOptions.push(clonedOptions); } // get clusterproperties based on childNodes var childEdgesOptions = []; for (var edgeId in childEdgesObj) { var clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge'); childEdgesOptions.push(clonedOptions); } clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); if (!clusterNodeProperties) { throw new Error('The processProperties function does not return properties!'); } } // check if we have an unique id; if (clusterNodeProperties.id === undefined) { clusterNodeProperties.id = 'cluster:' + util.randomUUID(); } var clusterId = clusterNodeProperties.id; if (clusterNodeProperties.label === undefined) { clusterNodeProperties.label = 'cluster'; } // give the clusterNode a postion if it does not have one. var pos = undefined; if (clusterNodeProperties.x === undefined) { pos = this._getClusterPosition(childNodesObj); clusterNodeProperties.x = pos.x; } if (clusterNodeProperties.y === undefined) { if (pos === undefined) { pos = this._getClusterPosition(childNodesObj); } clusterNodeProperties.y = pos.y; } // force the ID to remain the same clusterNodeProperties.id = clusterId; // create the clusterNode var clusterNode = this.body.functions.createNode(clusterNodeProperties, _componentsNodesCluster2['default']); clusterNode.isCluster = true; clusterNode.containedNodes = childNodesObj; clusterNode.containedEdges = childEdgesObj; // cache a copy from the cluster edge properties if we have to reconnect others later on clusterNode.clusterEdgeProperties = options.clusterEdgeProperties; // finally put the cluster node into global this.body.nodes[clusterNodeProperties.id] = clusterNode; // create the new edges that will connect to the cluster var newEdges = []; this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, options.clusterEdgeProperties); // disable the childEdges for (var edgeId in childEdgesObj) { if (childEdgesObj.hasOwnProperty(edgeId)) { if (this.body.edges[edgeId] !== undefined) { var edge = this.body.edges[edgeId]; edge.togglePhysics(false); edge.options.hidden = true; } } } // disable the childNodes for (var nodeId in childNodesObj) { if (childNodesObj.hasOwnProperty(nodeId)) { this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; this.body.nodes[nodeId].togglePhysics(false); this.body.nodes[nodeId].options.hidden = true; } } // push new edges to global for (var i = 0; i < newEdges.length; i++) { this.body.edges[newEdges[i].id] = newEdges[i]; this.body.edges[newEdges[i].id].connect(); } // set ID to undefined so no duplicates arise clusterNodeProperties.id = undefined; // wrap up if (refreshData === true) { this.body.emitter.emit('_dataChanged'); } } }, { key: 'isCluster', /** * Check if a node is a cluster. * @param nodeId * @returns {*} */ value: function isCluster(nodeId) { if (this.body.nodes[nodeId] !== undefined) { return this.body.nodes[nodeId].isCluster === true; } else { console.log('Node does not exist.'); return false; } } }, { key: '_getClusterPosition', /** * get the position of the cluster node based on what's inside * @param {object} childNodesObj | object with node objects, id as keys * @returns {{x: number, y: number}} * @private */ value: function _getClusterPosition(childNodesObj) { var childKeys = Object.keys(childNodesObj); var minX = childNodesObj[childKeys[0]].x; var maxX = childNodesObj[childKeys[0]].x; var minY = childNodesObj[childKeys[0]].y; var maxY = childNodesObj[childKeys[0]].y; var node = undefined; for (var i = 1; i < childKeys.length; i++) { node = childNodesObj[childKeys[i]]; minX = node.x < minX ? node.x : minX; maxX = node.x > maxX ? node.x : maxX; minY = node.y < minY ? node.y : minY; maxY = node.y > maxY ? node.y : maxY; } return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; } }, { key: 'openCluster', /** * Open a cluster by calling this function. * @param {String} clusterNodeId | the ID of the cluster node * @param {Boolean} refreshData | wrap up afterwards if not true */ value: function openCluster(clusterNodeId, options) { var refreshData = arguments[2] === undefined ? true : arguments[2]; // kill conditions if (clusterNodeId === undefined) { throw new Error('No clusterNodeId supplied to openCluster.'); } if (this.body.nodes[clusterNodeId] === undefined) { throw new Error('The clusterNodeId supplied to openCluster does not exist.'); } if (this.body.nodes[clusterNodeId].containedNodes === undefined) { console.log('The node:' + clusterNodeId + ' is not a cluster.'); return; } var clusterNode = this.body.nodes[clusterNodeId]; var containedNodes = clusterNode.containedNodes; var containedEdges = clusterNode.containedEdges; // allow the user to position the nodes after release. if (options !== undefined && options.releaseFunction !== undefined && typeof options.releaseFunction === 'function') { var positions = {}; var clusterPosition = { x: clusterNode.x, y: clusterNode.y }; for (var nodeId in containedNodes) { if (containedNodes.hasOwnProperty(nodeId)) { var containedNode = this.body.nodes[nodeId]; positions[nodeId] = { x: containedNode.x, y: containedNode.y }; } } var newPositions = options.releaseFunction(clusterPosition, positions); for (var nodeId in containedNodes) { if (containedNodes.hasOwnProperty(nodeId)) { var containedNode = this.body.nodes[nodeId]; if (newPositions[nodeId] !== undefined) { containedNode.x = newPositions[nodeId].x || clusterNode.x; containedNode.y = newPositions[nodeId].y || clusterNode.y; } } } } else { // copy the position from the cluster for (var nodeId in containedNodes) { if (containedNodes.hasOwnProperty(nodeId)) { var containedNode = this.body.nodes[nodeId]; containedNode = containedNodes[nodeId]; // inherit position containedNode.x = clusterNode.x; containedNode.y = clusterNode.y; } } } // release nodes for (var nodeId in containedNodes) { if (containedNodes.hasOwnProperty(nodeId)) { var containedNode = this.body.nodes[nodeId]; // inherit speed containedNode.vx = clusterNode.vx; containedNode.vy = clusterNode.vy; containedNode.options.hidden = false; containedNode.togglePhysics(true); delete this.clusteredNodes[nodeId]; } } // release edges for (var edgeId in containedEdges) { if (containedEdges.hasOwnProperty(edgeId)) { var edge = containedEdges[edgeId]; // if this edge was a temporary edge and it's connected nodes do not exist anymore, we remove it from the data if (this.body.nodes[edge.fromId] === undefined || this.body.nodes[edge.toId] === undefined) { edge.edgeType.cleanup(); // this removes the edge from node.edges, which is why edgeIds is formed edge.disconnect(); delete this.body.edges[edgeId]; } else { // one of the nodes connected to this edge is in a cluster. We give the edge to that cluster so it will be released when that cluster is opened. if (this.clusteredNodes[edge.fromId] !== undefined || this.clusteredNodes[edge.toId] !== undefined) { var fromId = undefined, toId = undefined; var clusteredNode = this.clusteredNodes[edge.fromId] || this.clusteredNodes[edge.toId]; var clusterId = clusteredNode.clusterId; var _clusterNode = this.body.nodes[clusterId]; _clusterNode.containedEdges[edgeId] = edge; if (this.clusteredNodes[edge.fromId] !== undefined) { fromId = clusterId; toId = edge.toId; } else { fromId = edge.fromId; toId = clusterId; } // if both from and to nodes are visible, we create a new temporary edge if (this.body.nodes[fromId].options.hidden !== true && this.body.nodes[toId].options.hidden !== true) { var clonedOptions = this._cloneOptions(edge, 'edge'); var id = 'clusterEdge:' + util.randomUUID(); util.deepExtend(clonedOptions, _clusterNode.clusterEdgeProperties); util.deepExtend(clonedOptions, { from: fromId, to: toId, hidden: false, physics: true, id: id }); var newEdge = this.body.functions.createEdge(clonedOptions); this.body.edges[id] = newEdge; this.body.edges[id].connect(); } } else { edge.options.hidden = false; edge.togglePhysics(true); } } } } // remove all temporary edges for (var i = 0; i < clusterNode.edges.length; i++) { var edgeId = clusterNode.edges[i].id; this.body.edges[edgeId].edgeType.cleanup(); // this removes the edge from node.edges, which is why edgeIds is formed this.body.edges[edgeId].disconnect(); delete this.body.edges[edgeId]; } // remove clusterNode delete this.body.nodes[clusterNodeId]; if (refreshData === true) { this.body.emitter.emit('_dataChanged'); } } }, { key: 'getNodesInCluster', value: function getNodesInCluster(clusterId) { var nodesArray = []; if (this.isCluster(clusterId) === true) { var containedNodes = this.body.nodes[clusterId].containedNodes; for (var nodeId in containedNodes) { if (containedNodes.hasOwnProperty(nodeId)) { nodesArray.push(nodeId); } } } return nodesArray; } }, { key: 'findNode', /** * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node * @param nodeId * @returns {Array} * @private */ value: function findNode(nodeId) { var stack = []; var max = 100; var counter = 0; while (this.clusteredNodes[nodeId] !== undefined && counter < max) { stack.push(this.clusteredNodes[nodeId].node); nodeId = this.clusteredNodes[nodeId].clusterId; counter++; } stack.push(this.body.nodes[nodeId]); return stack; } }, { key: '_getConnectedId', /** * Get the Id the node is connected to * @param edge * @param nodeId * @returns {*} * @private */ value: function _getConnectedId(edge, nodeId) { if (edge.toId != nodeId) { return edge.toId; } else if (edge.fromId != nodeId) { return edge.fromId; } else { return edge.fromId; } } }, { key: '_getHubSize', /** * We determine how many connections denote an important hub. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) * * @private */ value: function _getHubSize() { var average = 0; var averageSquared = 0; var hubCounter = 0; var largestHub = 0; for (var i = 0; i < this.body.nodeIndices.length; i++) { var node = this.body.nodes[this.body.nodeIndices[i]]; if (node.edges.length > largestHub) { largestHub = node.edges.length; } average += node.edges.length; averageSquared += Math.pow(node.edges.length, 2); hubCounter += 1; } average = average / hubCounter; averageSquared = averageSquared / hubCounter; var letiance = averageSquared - Math.pow(average, 2); var standardDeviation = Math.sqrt(letiance); var hubThreshold = Math.floor(average + 2 * standardDeviation); // always have at least one to cluster if (hubThreshold > largestHub) { hubThreshold = largestHub; } return hubThreshold; } }]); return ClusterEngine; })(); exports['default'] = ClusterEngine; module.exports = exports['default']; /***/ }, /* 98 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _Node2 = __webpack_require__(62); var _Node3 = _interopRequireDefault(_Node2); /** * */ var Cluster = (function (_Node) { function Cluster(options, body, imagelist, grouplist, globalOptions) { _classCallCheck(this, Cluster); _get(Object.getPrototypeOf(Cluster.prototype), 'constructor', this).call(this, options, body, imagelist, grouplist, globalOptions); this.isCluster = true; this.containedNodes = {}; this.containedEdges = {}; } _inherits(Cluster, _Node); return Cluster; })(_Node3['default']); exports['default'] = Cluster; module.exports = exports['default']; /***/ }, /* 99 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } if (typeof window !== 'undefined') { window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } var util = __webpack_require__(1); var CanvasRenderer = (function () { function CanvasRenderer(body, canvas) { _classCallCheck(this, CanvasRenderer); this.body = body; this.canvas = canvas; this.redrawRequested = false; this.renderTimer = undefined; this.requiresTimeout = true; this.renderingActive = false; this.renderRequests = 0; this.pixelRatio = undefined; this.allowRedrawRequests = true; this.dragging = false; this.options = {}; this.defaultOptions = { hideEdgesOnDrag: false, hideNodesOnDrag: false }; util.extend(this.options, this.defaultOptions); this._determineBrowserMethod(); this.bindEventListeners(); } _createClass(CanvasRenderer, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; this.body.emitter.on('dragStart', function () { _this.dragging = true; }); this.body.emitter.on('dragEnd', function () { return _this.dragging = false; }); this.body.emitter.on('_resizeNodes', function () { return _this._resizeNodes(); }); this.body.emitter.on('_redraw', function () { if (_this.renderingActive === false) { _this._redraw(); } }); this.body.emitter.on('_blockRedrawRequests', function () { _this.allowRedrawRequests = false; }); this.body.emitter.on('_allowRedrawRequests', function () { _this.allowRedrawRequests = true; }); this.body.emitter.on('_requestRedraw', this._requestRedraw.bind(this)); this.body.emitter.on('_startRendering', function () { _this.renderRequests += 1; _this.renderingActive = true; _this._startRendering(); }); this.body.emitter.on('_stopRendering', function () { _this.renderRequests -= 1; _this.renderingActive = _this.renderRequests > 0; _this.renderTimer = undefined; }); this.body.emitter.on('destroy', function () { _this.renderRequests = 0; _this.renderingActive = false; if (_this.requiresTimeout === true) { clearTimeout(_this.renderTimer); } else { cancelAnimationFrame(_this.renderTimer); } _this.body.emitter.off(); }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag']; util.selectiveDeepExtend(fields, this.options, options); } } }, { key: '_startRendering', value: function _startRendering() { if (this.renderingActive === true) { if (this.renderTimer === undefined) { if (this.requiresTimeout === true) { this.renderTimer = window.setTimeout(this._renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function } else { this.renderTimer = window.requestAnimationFrame(this._renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function } } } } }, { key: '_renderStep', value: function _renderStep() { if (this.renderingActive === true) { // reset the renderTimer so a new scheduled animation step can be set this.renderTimer = undefined; if (this.requiresTimeout === true) { // this schedules a new simulation step this._startRendering(); } this._redraw(); if (this.requiresTimeout === false) { // this schedules a new simulation step this._startRendering(); } } } }, { key: 'redraw', /** * Redraw the network with the current data * chart will be resized too. */ value: function redraw() { this.body.emitter.emit('setSize'); this._redraw(); } }, { key: '_requestRedraw', /** * Redraw the network with the current data * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. * @private */ value: function _requestRedraw() { var _this2 = this; if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedrawRequests === true) { this.redrawRequested = true; if (this.requiresTimeout === true) { window.setTimeout(function () { _this2._redraw(false); }, 0); } else { window.requestAnimationFrame(function () { _this2._redraw(false); }); } } } }, { key: '_redraw', value: function _redraw() { var hidden = arguments[0] === undefined ? false : arguments[0]; this.body.emitter.emit('initRedraw'); this.redrawRequested = false; var ctx = this.canvas.frame.canvas.getContext('2d'); // when the container div was hidden, this fixes it back up! if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) { this.canvas.setSize(); } if (this.pixelRatio === undefined) { this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); // clear the canvas var w = this.canvas.frame.canvas.clientWidth; var h = this.canvas.frame.canvas.clientHeight; ctx.clearRect(0, 0, w, h); // set scaling and translation ctx.save(); ctx.translate(this.body.view.translation.x, this.body.view.translation.y); ctx.scale(this.body.view.scale, this.body.view.scale); ctx.beginPath(); this.body.emitter.emit('beforeDrawing', ctx); ctx.closePath(); if (hidden === false) { if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { this._drawEdges(ctx); } } if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { this._drawNodes(ctx, hidden); } if (this.controlNodesActive === true) { this._drawControlNodes(ctx); } ctx.beginPath(); //this.physics.nodesSolver._debug(ctx,"#F00F0F"); this.body.emitter.emit('afterDrawing', ctx); ctx.closePath(); // restore original scaling and translation ctx.restore(); if (hidden === true) { ctx.clearRect(0, 0, w, h); } } }, { key: '_resizeNodes', /** * Redraw all nodes * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx * @param {Boolean} [alwaysShow] * @private */ value: function _resizeNodes() { var ctx = this.canvas.frame.canvas.getContext('2d'); if (this.pixelRatio === undefined) { this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); ctx.save(); ctx.translate(this.body.view.translation.x, this.body.view.translation.y); ctx.scale(this.body.view.scale, this.body.view.scale); var nodes = this.body.nodes; var node = undefined; // resize all nodes for (var nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { node = nodes[nodeId]; node.resize(ctx); node.updateBoundingBox(ctx); } } // restore original scaling and translation ctx.restore(); } }, { key: '_drawNodes', /** * Redraw all nodes * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx * @param {Boolean} [alwaysShow] * @private */ value: function _drawNodes(ctx) { var alwaysShow = arguments[1] === undefined ? false : arguments[1]; var nodes = this.body.nodes; var nodeIndices = this.body.nodeIndices; var node = undefined; var selected = []; var margin = 20; var topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin }); var bottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth + margin, y: this.canvas.frame.canvas.clientHeight + margin }); var viewableArea = { top: topLeft.y, left: topLeft.x, bottom: bottomRight.y, right: bottomRight.x }; // draw unselected nodes; for (var i = 0; i < nodeIndices.length; i++) { node = nodes[nodeIndices[i]]; // set selected nodes aside if (node.isSelected()) { selected.push(nodeIndices[i]); } else { if (alwaysShow === true) { node.draw(ctx); } else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) { node.draw(ctx); } else { node.updateBoundingBox(ctx); } } } // draw the selected nodes on top for (var i = 0; i < selected.length; i++) { node = nodes[selected[i]]; node.draw(ctx); } } }, { key: '_drawEdges', /** * Redraw all edges * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx * @private */ value: function _drawEdges(ctx) { var edges = this.body.edges; var edgeIndices = this.body.edgeIndices; var edge = undefined; for (var i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; if (edge.connected === true) { edge.draw(ctx); } } } }, { key: '_drawControlNodes', /** * Redraw all edges * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx * @private */ value: function _drawControlNodes(ctx) { var edges = this.body.edges; var edgeIndices = this.body.edgeIndices; var edge = undefined; for (var i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; edge._drawControlNodes(ctx); } } }, { key: '_determineBrowserMethod', /** * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because * some implementations (safari and IE9) did not support requestAnimationFrame * @private */ value: function _determineBrowserMethod() { if (typeof window !== 'undefined') { var browserType = navigator.userAgent.toLowerCase(); this.requiresTimeout = false; if (browserType.indexOf('msie 9.0') != -1) { // IE 9 this.requiresTimeout = true; } else if (browserType.indexOf('safari') != -1) { // safari if (browserType.indexOf('chrome') <= -1) { this.requiresTimeout = true; } } } else { this.requiresTimeout = true; } } }]); return CanvasRenderer; })(); exports['default'] = CanvasRenderer; module.exports = exports['default']; /***/ }, /* 100 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var Hammer = __webpack_require__(23); var hammerUtil = __webpack_require__(28); var util = __webpack_require__(1); /** * Create the main frame for the Network. * This function is executed once when a Network object is created. The frame * contains a canvas, and this canvas contains all objects like the axis and * nodes. * @private */ var Canvas = (function () { function Canvas(body) { _classCallCheck(this, Canvas); this.body = body; this.pixelRatio = 1; this.resizeTimer = undefined; this.resizeFunction = this._onResize.bind(this); this.options = {}; this.defaultOptions = { autoResize: true, height: '100%', width: '100%' }; util.extend(this.options, this.defaultOptions); this.bindEventListeners(); } _createClass(Canvas, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; // bind the events this.body.emitter.once('resize', function (obj) { if (obj.width !== 0) { _this.body.view.translation.x = obj.width * 0.5; } if (obj.height !== 0) { _this.body.view.translation.y = obj.height * 0.5; } }); this.body.emitter.on('setSize', this.setSize.bind(this)); this.body.emitter.on('destroy', function () { _this.hammerFrame.destroy(); _this.hammer.destroy(); _this._cleanUp(); }); } }, { key: 'setOptions', value: function setOptions(options) { var _this2 = this; if (options !== undefined) { var fields = ['width', 'height', 'autoResize']; util.selectiveDeepExtend(fields, this.options, options); } if (this.options.autoResize === true) { // automatically adapt to a changing size of the browser. this._cleanUp(); this.resizeTimer = setInterval(function () { var changed = _this2.setSize(); if (changed === true) { _this2.body.emitter.emit('_requestRedraw'); } }, 1000); this.resizeFunction = this._onResize.bind(this); util.addEventListener(window, 'resize', this.resizeFunction); } } }, { key: '_cleanUp', value: function _cleanUp() { // automatically adapt to a changing size of the browser. if (this.resizeTimer !== undefined) { clearInterval(this.resizeTimer); } util.removeEventListener(window, 'resize', this.resizeFunction); this.resizeFunction = undefined; } }, { key: '_onResize', value: function _onResize() { this.setSize(); this.body.emitter.emit('_redraw'); } }, { key: '_prepareValue', value: function _prepareValue(value) { if (typeof value === 'number') { return value + 'px'; } else if (typeof value === 'string') { if (value.indexOf('%') !== -1 || value.indexOf('px') !== -1) { return value; } else if (value.indexOf('%') === -1) { return value + 'px'; } } throw new Error('Could not use the value supplie for width or height:' + value); } }, { key: '_create', /** * Create the HTML */ value: function _create() { // remove all elements from the container element. while (this.body.container.hasChildNodes()) { this.body.container.removeChild(this.body.container.firstChild); } this.frame = document.createElement('div'); this.frame.className = 'vis-network'; this.frame.style.position = 'relative'; this.frame.style.overflow = 'hidden'; this.frame.tabIndex = 900; // tab index is required for keycharm to bind keystrokes to the div instead of the window ////////////////////////////////////////////////////////////////// this.frame.canvas = document.createElement('canvas'); this.frame.canvas.style.position = 'relative'; this.frame.appendChild(this.frame.canvas); if (!this.frame.canvas.getContext) { var noCanvas = document.createElement('DIV'); noCanvas.style.color = 'red'; noCanvas.style.fontWeight = 'bold'; noCanvas.style.padding = '10px'; noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; this.frame.canvas.appendChild(noCanvas); } else { var ctx = this.frame.canvas.getContext('2d'); this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); this.frame.canvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } // add the frame to the container element this.body.container.appendChild(this.frame); this.body.view.scale = 1; this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; this._bindHammer(); } }, { key: '_bindHammer', /** * This function binds hammer, it can be repeated over and over due to the uniqueness check. * @private */ value: function _bindHammer() { var _this3 = this; if (this.hammer !== undefined) { this.hammer.destroy(); } this.drag = {}; this.pinch = {}; // init hammer this.hammer = new Hammer(this.frame.canvas); this.hammer.get('pinch').set({ enable: true }); // enable to get better response, todo: test on mobile. //this.hammer.get('pan').set({threshold:2}); hammerUtil.onTouch(this.hammer, function (event) { _this3.body.eventListeners.onTouch(event); }); this.hammer.on('tap', function (event) { _this3.body.eventListeners.onTap(event); }); this.hammer.on('doubletap', function (event) { _this3.body.eventListeners.onDoubleTap(event); }); this.hammer.on('press', function (event) { _this3.body.eventListeners.onHold(event); }); this.hammer.on('panstart', function (event) { _this3.body.eventListeners.onDragStart(event); }); this.hammer.on('panmove', function (event) { _this3.body.eventListeners.onDrag(event); }); this.hammer.on('panend', function (event) { _this3.body.eventListeners.onDragEnd(event); }); this.hammer.on('pinch', function (event) { _this3.body.eventListeners.onPinch(event); }); // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? this.frame.canvas.addEventListener('mousewheel', function (event) { _this3.body.eventListeners.onMouseWheel(event); }); this.frame.canvas.addEventListener('DOMMouseScroll', function (event) { _this3.body.eventListeners.onMouseWheel(event); }); this.frame.canvas.addEventListener('mousemove', function (event) { _this3.body.eventListeners.onMouseMove(event); }); this.frame.canvas.addEventListener('contextmenu', function (event) { _this3.body.eventListeners.onContext(event); }); this.hammerFrame = new Hammer(this.frame); hammerUtil.onRelease(this.hammerFrame, function (event) { _this3.body.eventListeners.onRelease(event); }); } }, { key: 'setSize', /** * Set a new size for the network * @param {string} width Width in pixels or percentage (for example '800px' * or '50%') * @param {string} height Height in pixels or percentage (for example '400px' * or '30%') */ value: function setSize() { var width = arguments[0] === undefined ? this.options.width : arguments[0]; var height = arguments[1] === undefined ? this.options.height : arguments[1]; width = this._prepareValue(width); height = this._prepareValue(height); var emitEvent = false; var oldWidth = this.frame.canvas.width; var oldHeight = this.frame.canvas.height; if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { this.frame.style.width = width; this.frame.style.height = height; this.frame.canvas.style.width = '100%'; this.frame.canvas.style.height = '100%'; this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio); this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio); this.options.width = width; this.options.height = height; emitEvent = true; } else { // this would adapt the width of the canvas to the width from 100% if and only if // there is a change. if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio)) { this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio); emitEvent = true; } if (this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) { this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio); emitEvent = true; } } if (emitEvent === true) { this.body.emitter.emit('resize', { width: Math.round(this.frame.canvas.width / this.pixelRatio), height: Math.round(this.frame.canvas.height / this.pixelRatio), oldWidth: Math.round(oldWidth / this.pixelRatio), oldHeight: Math.round(oldHeight / this.pixelRatio) }); } return emitEvent; } }, { key: '_XconvertDOMtoCanvas', /** * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) * @param {number} x * @returns {number} * @private */ value: function _XconvertDOMtoCanvas(x) { return (x - this.body.view.translation.x) / this.body.view.scale; } }, { key: '_XconvertCanvasToDOM', /** * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to * the X coordinate in DOM-space (coordinate point in browser relative to the container div) * @param {number} x * @returns {number} * @private */ value: function _XconvertCanvasToDOM(x) { return x * this.body.view.scale + this.body.view.translation.x; } }, { key: '_YconvertDOMtoCanvas', /** * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) * @param {number} y * @returns {number} * @private */ value: function _YconvertDOMtoCanvas(y) { return (y - this.body.view.translation.y) / this.body.view.scale; } }, { key: '_YconvertCanvasToDOM', /** * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) * @param {number} y * @returns {number} * @private */ value: function _YconvertCanvasToDOM(y) { return y * this.body.view.scale + this.body.view.translation.y; } }, { key: 'canvasToDOM', /** * * @param {object} pos = {x: number, y: number} * @returns {{x: number, y: number}} * @constructor */ value: function canvasToDOM(pos) { return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; } }, { key: 'DOMtoCanvas', /** * * @param {object} pos = {x: number, y: number} * @returns {{x: number, y: number}} * @constructor */ value: function DOMtoCanvas(pos) { return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; } }]); return Canvas; })(); exports['default'] = Canvas; module.exports = exports['default']; /***/ }, /* 101 */ /***/ function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var util = __webpack_require__(1); var View = (function () { function View(body, canvas) { var _this = this; _classCallCheck(this, View); this.body = body; this.canvas = canvas; this.animationSpeed = 1 / this.renderRefreshRate; this.animationEasingFunction = "easeInOutQuint"; this.easingTime = 0; this.sourceScale = 0; this.targetScale = 0; this.sourceTranslation = 0; this.targetTranslation = 0; this.lockedOnNodeId = undefined; this.lockedOnNodeOffset = undefined; this.touchTime = 0; this.viewFunction = undefined; this.body.emitter.on("fit", this.fit.bind(this)); this.body.emitter.on("animationFinished", function () { _this.body.emitter.emit("_stopRendering"); }); this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); } _createClass(View, [{ key: "setOptions", value: function setOptions() { var options = arguments[0] === undefined ? {} : arguments[0]; this.options = options; } }, { key: "_getRange", /** * Find the center position of the network * @private */ value: function _getRange() { var specificNodes = arguments[0] === undefined ? [] : arguments[0]; var minY = 1000000000, maxY = -1000000000, minX = 1000000000, maxX = -1000000000, node; if (specificNodes.length > 0) { for (var i = 0; i < specificNodes.length; i++) { node = this.body.nodes[specificNodes[i]]; if (minX > node.shape.boundingBox.left) { minX = node.shape.boundingBox.left; } if (maxX < node.shape.boundingBox.right) { maxX = node.shape.boundingBox.right; } if (minY > node.shape.boundingBox.top) { minY = node.shape.boundingBox.top; } // top is negative, bottom is positive if (maxY < node.shape.boundingBox.bottom) { maxY = node.shape.boundingBox.bottom; } // top is negative, bottom is positive } } else { for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; if (minX > node.shape.boundingBox.left) { minX = node.shape.boundingBox.left; } if (maxX < node.shape.boundingBox.right) { maxX = node.shape.boundingBox.right; } if (minY > node.shape.boundingBox.top) { minY = node.shape.boundingBox.top; } // top is negative, bottom is positive if (maxY < node.shape.boundingBox.bottom) { maxY = node.shape.boundingBox.bottom; } // top is negative, bottom is positive } } } if (minX === 1000000000 && maxX === -1000000000 && minY === 1000000000 && maxY === -1000000000) { minY = 0, maxY = 0, minX = 0, maxX = 0; } return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; } }, { key: "_findCenter", /** * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; * @returns {{x: number, y: number}} * @private */ value: function _findCenter(range) { return { x: 0.5 * (range.maxX + range.minX), y: 0.5 * (range.maxY + range.minY) }; } }, { key: "fit", /** * This function zooms out to fit all data on screen based on amount of nodes * @param {Object} Options * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; */ value: function fit() { var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; var initialZoom = arguments[1] === undefined ? false : arguments[1]; var range; var zoomLevel; if (initialZoom === true) { // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. var positionDefined = 0; for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { var node = this.body.nodes[nodeId]; if (node.predefinedPosition === true) { positionDefined += 1; } } } if (positionDefined > 0.5 * this.body.nodeIndices.length) { this.fit(options, false); return; } range = this._getRange(options.nodes); var numberOfNodes = this.body.nodeIndices.length; zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. // correct for larger canvasses. var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); zoomLevel *= factor; } else { this.body.emitter.emit("_resizeNodes"); range = this._getRange(options.nodes); var xDistance = Math.abs(range.maxX - range.minX) * 1.1; var yDistance = Math.abs(range.maxY - range.minY) * 1.1; var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; } if (zoomLevel > 1) { zoomLevel = 1; } else if (zoomLevel === 0) { zoomLevel = 1; } var center = this._findCenter(range); var animationOptions = { position: center, scale: zoomLevel, animation: options.animation }; this.moveTo(animationOptions); } }, { key: "focus", // animation /** * Center a node in view. * * @param {Number} nodeId * @param {Number} [options] */ value: function focus(nodeId) { var options = arguments[1] === undefined ? {} : arguments[1]; if (this.body.nodes[nodeId] !== undefined) { var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; options.position = nodePosition; options.lockedOnNode = nodeId; this.moveTo(options); } else { console.log("Node: " + nodeId + " cannot be found."); } } }, { key: "moveTo", /** * * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels * | options.scale = Number // scale to move to * | options.position = {x:Number, y:Number} // position to move to * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to */ value: function moveTo(options) { if (options === undefined) { options = {}; return; } if (options.offset === undefined) { options.offset = { x: 0, y: 0 }; } if (options.offset.x === undefined) { options.offset.x = 0; } if (options.offset.y === undefined) { options.offset.y = 0; } if (options.scale === undefined) { options.scale = this.body.view.scale; } if (options.position === undefined) { options.position = this.getViewPosition(); } if (options.animation === undefined) { options.animation = { duration: 0 }; } if (options.animation === false) { options.animation = { duration: 0 }; } if (options.animation === true) { options.animation = {}; } if (options.animation.duration === undefined) { options.animation.duration = 1000; } // default duration if (options.animation.easingFunction === undefined) { options.animation.easingFunction = "easeInOutQuad"; } // default easing function this.animateView(options); } }, { key: "animateView", /** * * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels * | options.time = Number // animation time in milliseconds * | options.scale = Number // scale to animate to * | options.position = {x:Number, y:Number} // position to animate to * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, * // easeInCubic, easeOutCubic, easeInOutCubic, * // easeInQuart, easeOutQuart, easeInOutQuart, * // easeInQuint, easeOutQuint, easeInOutQuint */ value: function animateView(options) { if (options === undefined) { return; } this.animationEasingFunction = options.animation.easingFunction; // release if something focussed on the node this.releaseNode(); if (options.locked === true) { this.lockedOnNodeId = options.lockedOnNode; this.lockedOnNodeOffset = options.offset; } // forcefully complete the old animation if it was still running if (this.easingTime != 0) { this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. } this.sourceScale = this.body.view.scale; this.sourceTranslation = this.body.view.translation; this.targetScale = options.scale; // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw // but at least then we'll have the target transition this.body.view.scale = this.targetScale; var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node x: viewCenter.x - options.position.x, y: viewCenter.y - options.position.y }; this.targetTranslation = { x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y }; // if the time is set to 0, don't do an animation if (options.animation.duration === 0) { if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this); this.body.emitter.on("initRedraw", this.viewFunction); } else { this.body.view.scale = this.targetScale; this.body.view.translation = this.targetTranslation; this.body.emitter.emit("_requestRedraw"); } } else { this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's this.animationEasingFunction = options.animation.easingFunction; this.viewFunction = this._transitionRedraw.bind(this); this.body.emitter.on("initRedraw", this.viewFunction); this.body.emitter.emit("_startRendering"); } } }, { key: "_lockedRedraw", /** * used to animate smoothly by hijacking the redraw function. * @private */ value: function _lockedRedraw() { var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node x: viewCenter.x - nodePosition.x, y: viewCenter.y - nodePosition.y }; var sourceTranslation = this.body.view.translation; var targetTranslation = { x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y }; this.body.view.translation = targetTranslation; } }, { key: "releaseNode", value: function releaseNode() { if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { this.body.emitter.off("initRedraw", this.viewFunction); this.lockedOnNodeId = undefined; this.lockedOnNodeOffset = undefined; } } }, { key: "_transitionRedraw", /** * * @param easingTime * @private */ value: function _transitionRedraw() { var finished = arguments[0] === undefined ? false : arguments[0]; this.easingTime += this.animationSpeed; this.easingTime = finished === true ? 1 : this.easingTime; var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; this.body.view.translation = { x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress }; // cleanup if (this.easingTime >= 1) { this.body.emitter.off("initRedraw", this.viewFunction); this.easingTime = 0; if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this); this.body.emitter.on("initRedraw", this.viewFunction); } this.body.emitter.emit("animationFinished"); } } }, { key: "getScale", value: function getScale() { return this.body.view.scale; } }, { key: "getViewPosition", value: function getViewPosition() { return this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); } }]); return View; })(); exports["default"] = View; module.exports = exports["default"]; /***/ }, /* 102 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _componentsNavigationHandler = __webpack_require__(103); var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); var _componentsPopup = __webpack_require__(104); var _componentsPopup2 = _interopRequireDefault(_componentsPopup); var util = __webpack_require__(1); var InteractionHandler = (function () { function InteractionHandler(body, canvas, selectionHandler) { _classCallCheck(this, InteractionHandler); this.body = body; this.canvas = canvas; this.selectionHandler = selectionHandler; this.navigationHandler = new _componentsNavigationHandler2['default'](body, canvas); // bind the events from hammer to functions in this object this.body.eventListeners.onTap = this.onTap.bind(this); this.body.eventListeners.onTouch = this.onTouch.bind(this); this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); this.body.eventListeners.onHold = this.onHold.bind(this); this.body.eventListeners.onDragStart = this.onDragStart.bind(this); this.body.eventListeners.onDrag = this.onDrag.bind(this); this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); this.body.eventListeners.onPinch = this.onPinch.bind(this); this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); this.body.eventListeners.onRelease = this.onRelease.bind(this); this.body.eventListeners.onContext = this.onContext.bind(this); this.touchTime = 0; this.drag = {}; this.pinch = {}; this.popup = undefined; this.popupObj = undefined; this.popupTimer = undefined; this.body.functions.getPointer = this.getPointer.bind(this); this.options = {}; this.defaultOptions = { dragNodes: true, dragView: true, hover: false, keyboard: { enabled: false, speed: { x: 10, y: 10, zoom: 0.02 }, bindToWindow: true }, navigationButtons: false, tooltipDelay: 300, zoomView: true }; util.extend(this.options, this.defaultOptions); this.bindEventListeners(); } _createClass(InteractionHandler, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; this.body.emitter.on('destroy', function () { clearTimeout(_this.popupTimer); delete _this.body.functions.getPointer; }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { // extend all but the values in fields var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag', 'keyboard', 'multiselect', 'selectable', 'selectConnectedEdges']; util.selectiveNotDeepExtend(fields, this.options, options); // merge the keyboard options in. util.mergeOptions(this.options, options, 'keyboard'); if (options.tooltip) { util.extend(this.options.tooltip, options.tooltip); if (options.tooltip.color) { this.options.tooltip.color = util.parseColor(options.tooltip.color); } } } this.navigationHandler.setOptions(this.options); } }, { key: 'getPointer', /** * Get the pointer location from a touch location * @param {{x: Number, y: Number}} touch * @return {{x: Number, y: Number}} pointer * @private */ value: function getPointer(touch) { return { x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) }; } }, { key: 'onTouch', /** * On start of a touch gesture, store the pointer * @param event * @private */ value: function onTouch(event) { if (new Date().valueOf() - this.touchTime > 50) { this.drag.pointer = this.getPointer(event.center); this.drag.pinched = false; this.pinch.scale = this.body.view.scale; // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) this.touchTime = new Date().valueOf(); } } }, { key: 'onTap', /** * handle tap/click event: select/unselect a node * @private */ value: function onTap(event) { var pointer = this.getPointer(event.center); var multiselect = this.selectionHandler.options.multiselect && (event.changedPointers[0].ctrlKey || event.changedPointers[0].metaKey); this.checkSelectionChanges(pointer, event, multiselect); this.selectionHandler._generateClickEvent('click', event, pointer); } }, { key: 'onDoubleTap', /** * handle doubletap event * @private */ value: function onDoubleTap(event) { var pointer = this.getPointer(event.center); this.selectionHandler._generateClickEvent('doubleClick', event, pointer); } }, { key: 'onHold', /** * handle long tap event: multi select nodes * @private */ value: function onHold(event) { var pointer = this.getPointer(event.center); var multiselect = this.selectionHandler.options.multiselect; this.checkSelectionChanges(pointer, event, multiselect); this.selectionHandler._generateClickEvent('click', event, pointer); this.selectionHandler._generateClickEvent('hold', event, pointer); } }, { key: 'onRelease', /** * handle the release of the screen * * @private */ value: function onRelease(event) { if (new Date().valueOf() - this.touchTime > 10) { var pointer = this.getPointer(event.center); this.selectionHandler._generateClickEvent('release', event, pointer); // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) this.touchTime = new Date().valueOf(); } } }, { key: 'onContext', value: function onContext(event) { var pointer = this.getPointer({ x: event.clientX, y: event.clientY }); this.selectionHandler._generateClickEvent('oncontext', event, pointer); } }, { key: 'checkSelectionChanges', /** * * @param pointer * @param add */ value: function checkSelectionChanges(pointer, event) { var add = arguments[2] === undefined ? false : arguments[2]; var previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); var previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount(); var previousSelection = this.selectionHandler.getSelection(); var selected = undefined; if (add === true) { selected = this.selectionHandler.selectAdditionalOnPoint(pointer); } else { selected = this.selectionHandler.selectOnPoint(pointer); } var selectedEdgesCount = this.selectionHandler._getSelectedEdgeCount(); var selectedNodesCount = this.selectionHandler._getSelectedNodeCount(); var currentSelection = this.selectionHandler.getSelection(); var _determineIfDifferent2 = this._determineIfDifferent(previousSelection, currentSelection); var nodesChanges = _determineIfDifferent2.nodesChanges; var edgesChanges = _determineIfDifferent2.edgesChanges; if (selectedNodesCount - previouslySelectedNodeCount > 0) { // node was selected this.selectionHandler._generateClickEvent('selectNode', event, pointer); selected = true; } else if (selectedNodesCount - previouslySelectedNodeCount < 0) { // node was deselected this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection); selected = true; } else if (selectedNodesCount === previouslySelectedNodeCount && nodesChanges === true) { this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection); this.selectionHandler._generateClickEvent('selectNode', event, pointer); selected = true; } if (selectedEdgesCount - previouslySelectedEdgeCount > 0) { // edge was selected this.selectionHandler._generateClickEvent('selectEdge', event, pointer); selected = true; } else if (selectedEdgesCount - previouslySelectedEdgeCount < 0) { // edge was deselected this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection); selected = true; } else if (selectedEdgesCount === previouslySelectedEdgeCount && edgesChanges === true) { this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection); this.selectionHandler._generateClickEvent('selectEdge', event, pointer); selected = true; } if (selected === true) { // select or unselect this.selectionHandler._generateClickEvent('select', event, pointer); } } }, { key: '_determineIfDifferent', /** * This function checks if the nodes and edges previously selected have changed. * @param previousSelection * @param currentSelection * @returns {{nodesChanges: boolean, edgesChanges: boolean}} * @private */ value: function _determineIfDifferent(previousSelection, currentSelection) { var nodesChanges = false; var edgesChanges = false; for (var i = 0; i < previousSelection.nodes.length; i++) { if (currentSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) { nodesChanges = true; } } for (var i = 0; i < currentSelection.nodes.length; i++) { if (previousSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) { nodesChanges = true; } } for (var i = 0; i < previousSelection.edges.length; i++) { if (currentSelection.edges.indexOf(previousSelection.edges[i]) === -1) { edgesChanges = true; } } for (var i = 0; i < currentSelection.edges.length; i++) { if (previousSelection.edges.indexOf(previousSelection.edges[i]) === -1) { edgesChanges = true; } } return { nodesChanges: nodesChanges, edgesChanges: edgesChanges }; } }, { key: 'onDragStart', /** * This function is called by onDragStart. * It is separated out because we can then overload it for the datamanipulation system. * * @private */ value: function onDragStart(event) { //in case the touch event was triggered on an external div, do the initial touch now. if (this.drag.pointer === undefined) { this.onTouch(event); } // note: drag.pointer is set in onTouch to get the initial touch location var node = this.selectionHandler.getNodeAt(this.drag.pointer); this.drag.dragging = true; this.drag.selection = []; this.drag.translation = util.extend({}, this.body.view.translation); // copy the object this.drag.nodeId = undefined; if (node !== undefined && this.options.dragNodes === true) { this.drag.nodeId = node.id; // select the clicked node if not yet selected if (node.isSelected() === false) { this.selectionHandler.unselectAll(); this.selectionHandler.selectObject(node); } // after select to contain the node this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer); var selection = this.selectionHandler.selectionObj.nodes; // create an array with the selected nodes and their original location and status for (var nodeId in selection) { if (selection.hasOwnProperty(nodeId)) { var object = selection[nodeId]; var s = { id: object.id, node: object, // store original x, y, xFixed and yFixed, make the node temporarily Fixed x: object.x, y: object.y, xFixed: object.options.fixed.x, yFixed: object.options.fixed.y }; object.options.fixed.x = true; object.options.fixed.y = true; this.drag.selection.push(s); } } } else { // fallback if no node is selected and thus the view is dragged. this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer, undefined, true); } } }, { key: 'onDrag', /** * handle drag event * @private */ value: function onDrag(event) { var _this2 = this; if (this.drag.pinched === true) { return; } // remove the focus on node if it is focussed on by the focusOnNode this.body.emitter.emit('unlockNode'); var pointer = this.getPointer(event.center); var selection = this.drag.selection; if (selection && selection.length && this.options.dragNodes === true) { (function () { _this2.selectionHandler._generateClickEvent('dragging', event, pointer); // calculate delta's and new location var deltaX = pointer.x - _this2.drag.pointer.x; var deltaY = pointer.y - _this2.drag.pointer.y; // update position of all selected nodes selection.forEach(function (selection) { var node = selection.node; // only move the node if it was not fixed initially if (selection.xFixed === false) { node.x = _this2.canvas._XconvertDOMtoCanvas(_this2.canvas._XconvertCanvasToDOM(selection.x) + deltaX); } // only move the node if it was not fixed initially if (selection.yFixed === false) { node.y = _this2.canvas._YconvertDOMtoCanvas(_this2.canvas._YconvertCanvasToDOM(selection.y) + deltaY); } }); // start the simulation of the physics _this2.body.emitter.emit('startSimulation'); })(); } else { // move the network if (this.options.dragView === true) { this.selectionHandler._generateClickEvent('dragging', event, pointer, undefined, true); // if the drag was not started properly because the click started outside the network div, start it now. if (this.drag.pointer === undefined) { this._handleDragStart(event); return; } var diffX = pointer.x - this.drag.pointer.x; var diffY = pointer.y - this.drag.pointer.y; this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; this.body.emitter.emit('_redraw'); } } } }, { key: 'onDragEnd', /** * handle drag start event * @private */ value: function onDragEnd(event) { this.drag.dragging = false; var selection = this.drag.selection; if (selection && selection.length) { this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center)); selection.forEach(function (s) { // restore original xFixed and yFixed s.node.options.fixed.x = s.xFixed; s.node.options.fixed.y = s.yFixed; }); this.body.emitter.emit('startSimulation'); } else { this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center), undefined, true); this.body.emitter.emit('_requestRedraw'); } } }, { key: 'onPinch', /** * Handle pinch event * @param event * @private */ value: function onPinch(event) { var pointer = this.getPointer(event.center); this.drag.pinched = true; if (this.pinch['scale'] === undefined) { this.pinch.scale = 1; } // TODO: enabled moving while pinching? var scale = this.pinch.scale * event.scale; this.zoom(scale, pointer); } }, { key: 'zoom', /** * Zoom the network in or out * @param {Number} scale a number around 1, and between 0.01 and 10 * @param {{x: Number, y: Number}} pointer Position on screen * @return {Number} appliedScale scale is limited within the boundaries * @private */ value: function zoom(scale, pointer) { if (this.options.zoomView === true) { var scaleOld = this.body.view.scale; if (scale < 0.00001) { scale = 0.00001; } if (scale > 10) { scale = 10; } var preScaleDragPointer = undefined; if (this.drag !== undefined) { if (this.drag.dragging === true) { preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); } } // + this.canvas.frame.canvas.clientHeight / 2 var translation = this.body.view.translation; var scaleFrac = scale / scaleOld; var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; this.body.view.scale = scale; this.body.view.translation = { x: tx, y: ty }; if (preScaleDragPointer != undefined) { var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); this.drag.pointer.x = postScaleDragPointer.x; this.drag.pointer.y = postScaleDragPointer.y; } this.body.emitter.emit('_requestRedraw'); if (scaleOld < scale) { this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale }); } else { this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale }); } } } }, { key: 'onMouseWheel', /** * Event handler for mouse wheel event, used to zoom the timeline * See http://adomas.org/javascript-mouse-wheel/ * https://github.com/EightMedia/hammer.js/issues/256 * @param {MouseEvent} event * @private */ value: function onMouseWheel(event) { // retrieve delta var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ delta = event.wheelDelta / 120; } else if (event.detail) { /* Mozilla case. */ // In Mozilla, sign of delta is different than in IE. // Also, delta is multiple of 3. delta = -event.detail / 3; } // If delta is nonzero, handle it. // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. if (delta !== 0) { // calculate the new scale var scale = this.body.view.scale; var zoom = delta / 10; if (delta < 0) { zoom = zoom / (1 - zoom); } scale *= 1 + zoom; // calculate the pointer location var pointer = this.getPointer({ x: event.clientX, y: event.clientY }); // apply the new scale this.zoom(scale, pointer); } // Prevent default actions caused by mouse wheel. event.preventDefault(); } }, { key: 'onMouseMove', /** * Mouse move handler for checking whether the title moves over a node with a title. * @param {Event} event * @private */ value: function onMouseMove(event) { var _this3 = this; var pointer = this.getPointer({ x: event.clientX, y: event.clientY }); var popupVisible = false; // check if the previously selected node is still selected if (this.popup !== undefined) { if (this.popup.hidden === false) { this._checkHidePopup(pointer); } // if the popup was not hidden above if (this.popup.hidden === false) { popupVisible = true; this.popup.setPosition(pointer.x + 3, pointer.y - 5); this.popup.show(); } } // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over. if (this.options.keyboard.bindToWindow === false && this.options.keyboard.enabled === true) { this.canvas.frame.focus(); } // start a timeout that will check if the mouse is positioned above an element if (popupVisible === false) { if (this.popupTimer !== undefined) { clearInterval(this.popupTimer); // stop any running calculationTimer this.popupTimer = undefined; } if (!this.drag.dragging) { this.popupTimer = setTimeout(function () { return _this3._checkShowPopup(pointer); }, this.options.tooltipDelay); } } /** * Adding hover highlights */ if (this.options.hover === true) { // adding hover highlights var obj = this.selectionHandler.getNodeAt(pointer); if (obj === undefined) { obj = this.selectionHandler.getEdgeAt(pointer); } this.selectionHandler.hoverObject(obj); } } }, { key: '_checkShowPopup', /** * Check if there is an element on the given position in the network * (a node or edge). If so, and if this element has a title, * show a popup window with its title. * * @param {{x:Number, y:Number}} pointer * @private */ value: function _checkShowPopup(pointer) { var x = this.canvas._XconvertDOMtoCanvas(pointer.x); var y = this.canvas._YconvertDOMtoCanvas(pointer.y); var pointerObj = { left: x, top: y, right: x, bottom: y }; var previousPopupObjId = this.popupObj === undefined ? undefined : this.popupObj.id; var nodeUnderCursor = false; var popupType = 'node'; // check if a node is under the cursor. if (this.popupObj === undefined) { // search the nodes for overlap, select the top one in case of multiple nodes var nodeIndices = this.body.nodeIndices; var nodes = this.body.nodes; var node = undefined; var overlappingNodes = []; for (var i = 0; i < nodeIndices.length; i++) { node = nodes[nodeIndices[i]]; if (node.isOverlappingWith(pointerObj) === true) { if (node.getTitle() !== undefined) { overlappingNodes.push(nodeIndices[i]); } } } if (overlappingNodes.length > 0) { // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; // if you hover over a node, the title of the edge is not supposed to be shown. nodeUnderCursor = true; } } if (this.popupObj === undefined && nodeUnderCursor === false) { // search the edges for overlap var edgeIndices = this.body.edgeIndices; var edges = this.body.edges; var edge = undefined; var overlappingEdges = []; for (var i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; if (edge.isOverlappingWith(pointerObj) === true) { if (edge.connected === true && edge.getTitle() !== undefined) { overlappingEdges.push(edgeIndices[i]); } } } if (overlappingEdges.length > 0) { this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; popupType = 'edge'; } } if (this.popupObj !== undefined) { // show popup message window if (this.popupObj.id !== previousPopupObjId) { if (this.popup === undefined) { this.popup = new _componentsPopup2['default'](this.canvas.frame); } this.popup.popupTargetType = popupType; this.popup.popupTargetId = this.popupObj.id; // adjust a small offset such that the mouse cursor is located in the // bottom left location of the popup, and you can easily move over the // popup area this.popup.setPosition(pointer.x + 3, pointer.y - 5); this.popup.setText(this.popupObj.getTitle()); this.popup.show(); this.body.emitter.emit('showPopup', this.popupObj.id); } } else { if (this.popup !== undefined) { this.popup.hide(); this.body.emitter.emit('hidePopup'); } } } }, { key: '_checkHidePopup', /** * Check if the popup must be hidden, which is the case when the mouse is no * longer hovering on the object * @param {{x:Number, y:Number}} pointer * @private */ value: function _checkHidePopup(pointer) { var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); var stillOnObj = false; if (this.popup.popupTargetType === 'node') { if (this.body.nodes[this.popup.popupTargetId] !== undefined) { stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. // we initially only check stillOnObj because this is much faster. if (stillOnObj === true) { var overNode = this.selectionHandler.getNodeAt(pointer); stillOnObj = overNode.id === this.popup.popupTargetId; } } } else { if (this.selectionHandler.getNodeAt(pointer) === undefined) { if (this.body.edges[this.popup.popupTargetId] !== undefined) { stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); } } } if (stillOnObj === false) { this.popupObj = undefined; this.popup.hide(); this.body.emitter.emit('hidePopup'); } } }]); return InteractionHandler; })(); exports['default'] = InteractionHandler; module.exports = exports['default']; /***/ }, /* 103 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); var Hammer = __webpack_require__(23); var hammerUtil = __webpack_require__(28); var keycharm = __webpack_require__(43); var NavigationHandler = (function () { function NavigationHandler(body, canvas) { var _this = this; _classCallCheck(this, NavigationHandler); this.body = body; this.canvas = canvas; this.iconsCreated = false; this.navigationHammers = []; this.boundFunctions = {}; this.touchTime = 0; this.activated = false; this.body.emitter.on('release', function () { _this._stopMovement(); }); this.body.emitter.on('activate', function () { _this.activated = true;_this.configureKeyboardBindings(); }); this.body.emitter.on('deactivate', function () { _this.activated = false;_this.configureKeyboardBindings(); }); this.body.emitter.on('destroy', function () { if (_this.keycharm !== undefined) { _this.keycharm.destroy(); } }); this.options = {}; } _createClass(NavigationHandler, [{ key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { this.options = options; this.create(); } } }, { key: 'create', value: function create() { if (this.options.navigationButtons === true) { if (this.iconsCreated === false) { this.loadNavigationElements(); } } else if (this.iconsCreated === true) { this.cleanNavigation(); } this.configureKeyboardBindings(); } }, { key: 'cleanNavigation', value: function cleanNavigation() { // clean hammer bindings if (this.navigationHammers.length != 0) { for (var i = 0; i < this.navigationHammers.length; i++) { this.navigationHammers[i].destroy(); } this.navigationHammers = []; } // clean up previous navigation items if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); } this.iconsCreated = false; } }, { key: 'loadNavigationElements', /** * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. * * @private */ value: function loadNavigationElements() { this.cleanNavigation(); this.navigationDOM = {}; var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; this.navigationDOM['wrapper'] = document.createElement('div'); this.navigationDOM['wrapper'].className = 'vis-navigation'; this.canvas.frame.appendChild(this.navigationDOM['wrapper']); for (var i = 0; i < navigationDivs.length; i++) { this.navigationDOM[navigationDivs[i]] = document.createElement('div'); this.navigationDOM[navigationDivs[i]].className = 'vis-button vis-' + navigationDivs[i]; this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]); var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); if (navigationDivActions[i] === '_fit') { hammerUtil.onTouch(hammer, this._fit.bind(this)); } else { hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); } this.navigationHammers.push(hammer); } this.iconsCreated = true; } }, { key: 'bindToRedraw', value: function bindToRedraw(action) { if (this.boundFunctions[action] === undefined) { this.boundFunctions[action] = this[action].bind(this); this.body.emitter.on('initRedraw', this.boundFunctions[action]); this.body.emitter.emit('_startRendering'); } } }, { key: 'unbindFromRedraw', value: function unbindFromRedraw(action) { if (this.boundFunctions[action] !== undefined) { this.body.emitter.off('initRedraw', this.boundFunctions[action]); this.body.emitter.emit('_stopRendering'); delete this.boundFunctions[action]; } } }, { key: '_fit', /** * this stops all movement induced by the navigation buttons * * @private */ value: function _fit() { if (new Date().valueOf() - this.touchTime > 700) { // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) this.body.emitter.emit('fit', { duration: 700 }); this.touchTime = new Date().valueOf(); } } }, { key: '_stopMovement', /** * this stops all movement induced by the navigation buttons * * @private */ value: function _stopMovement() { for (var boundAction in this.boundFunctions) { if (this.boundFunctions.hasOwnProperty(boundAction)) { this.body.emitter.off('initRedraw', this.boundFunctions[boundAction]); this.body.emitter.emit('_stopRendering'); } } this.boundFunctions = {}; } }, { key: '_moveUp', value: function _moveUp() { this.body.view.translation.y += this.options.keyboard.speed.y; } }, { key: '_moveDown', value: function _moveDown() { this.body.view.translation.y -= this.options.keyboard.speed.y; } }, { key: '_moveLeft', value: function _moveLeft() { this.body.view.translation.x += this.options.keyboard.speed.x; } }, { key: '_moveRight', value: function _moveRight() { this.body.view.translation.x -= this.options.keyboard.speed.x; } }, { key: '_zoomIn', value: function _zoomIn() { this.body.view.scale *= 1 + this.options.keyboard.speed.zoom; } }, { key: '_zoomOut', value: function _zoomOut() { this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; } }, { key: 'configureKeyboardBindings', /** * bind all keys using keycharm. */ value: function configureKeyboardBindings() { var _this2 = this; if (this.keycharm !== undefined) { this.keycharm.destroy(); } if (this.options.keyboard.enabled === true) { if (this.options.keyboard.bindToWindow === true) { this.keycharm = keycharm({ container: window, preventDefault: true }); } else { this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); } this.keycharm.reset(); if (this.activated === true) { this.keycharm.bind('up', function () { _this2.bindToRedraw('_moveUp'); }, 'keydown'); this.keycharm.bind('down', function () { _this2.bindToRedraw('_moveDown'); }, 'keydown'); this.keycharm.bind('left', function () { _this2.bindToRedraw('_moveLeft'); }, 'keydown'); this.keycharm.bind('right', function () { _this2.bindToRedraw('_moveRight'); }, 'keydown'); this.keycharm.bind('=', function () { _this2.bindToRedraw('_zoomIn'); }, 'keydown'); this.keycharm.bind('num+', function () { _this2.bindToRedraw('_zoomIn'); }, 'keydown'); this.keycharm.bind('num-', function () { _this2.bindToRedraw('_zoomOut'); }, 'keydown'); this.keycharm.bind('-', function () { _this2.bindToRedraw('_zoomOut'); }, 'keydown'); this.keycharm.bind('[', function () { _this2.bindToRedraw('_zoomOut'); }, 'keydown'); this.keycharm.bind(']', function () { _this2.bindToRedraw('_zoomIn'); }, 'keydown'); this.keycharm.bind('pageup', function () { _this2.bindToRedraw('_zoomIn'); }, 'keydown'); this.keycharm.bind('pagedown', function () { _this2.bindToRedraw('_zoomOut'); }, 'keydown'); this.keycharm.bind('up', function () { _this2.unbindFromRedraw('_moveUp'); }, 'keyup'); this.keycharm.bind('down', function () { _this2.unbindFromRedraw('_moveDown'); }, 'keyup'); this.keycharm.bind('left', function () { _this2.unbindFromRedraw('_moveLeft'); }, 'keyup'); this.keycharm.bind('right', function () { _this2.unbindFromRedraw('_moveRight'); }, 'keyup'); this.keycharm.bind('=', function () { _this2.unbindFromRedraw('_zoomIn'); }, 'keyup'); this.keycharm.bind('num+', function () { _this2.unbindFromRedraw('_zoomIn'); }, 'keyup'); this.keycharm.bind('num-', function () { _this2.unbindFromRedraw('_zoomOut'); }, 'keyup'); this.keycharm.bind('-', function () { _this2.unbindFromRedraw('_zoomOut'); }, 'keyup'); this.keycharm.bind('[', function () { _this2.unbindFromRedraw('_zoomOut'); }, 'keyup'); this.keycharm.bind(']', function () { _this2.unbindFromRedraw('_zoomIn'); }, 'keyup'); this.keycharm.bind('pageup', function () { _this2.unbindFromRedraw('_zoomIn'); }, 'keyup'); this.keycharm.bind('pagedown', function () { _this2.unbindFromRedraw('_zoomOut'); }, 'keyup'); } } } }]); return NavigationHandler; })(); exports['default'] = NavigationHandler; module.exports = exports['default']; /***/ }, /* 104 */ /***/ function(module, exports) { /** * Popup is a class to create a popup window with some text * @param {Element} container The container object. * @param {Number} [x] * @param {Number} [y] * @param {String} [text] * @param {Object} [style] An object containing borderColor, * backgroundColor, etc. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var Popup = (function () { function Popup(container) { _classCallCheck(this, Popup); this.container = container; this.x = 0; this.y = 0; this.padding = 5; this.hidden = false; // create the frame this.frame = document.createElement('div'); this.frame.className = 'vis-network-tooltip'; this.container.appendChild(this.frame); } _createClass(Popup, [{ key: 'setPosition', /** * @param {number} x Horizontal position of the popup window * @param {number} y Vertical position of the popup window */ value: function setPosition(x, y) { this.x = parseInt(x); this.y = parseInt(y); } }, { key: 'setText', /** * Set the content for the popup window. This can be HTML code or text. * @param {string | Element} content */ value: function setText(content) { if (content instanceof Element) { this.frame.innerHTML = ''; this.frame.appendChild(content); } else { this.frame.innerHTML = content; // string containing text or HTML } } }, { key: 'show', /** * Show the popup window * @param {boolean} [doShow] Show or hide the window */ value: function show(doShow) { if (doShow === undefined) { doShow = true; } if (doShow === true) { var height = this.frame.clientHeight; var width = this.frame.clientWidth; var maxHeight = this.frame.parentNode.clientHeight; var maxWidth = this.frame.parentNode.clientWidth; var top = this.y - height; if (top + height + this.padding > maxHeight) { top = maxHeight - height - this.padding; } if (top < this.padding) { top = this.padding; } var left = this.x; if (left + width + this.padding > maxWidth) { left = maxWidth - width - this.padding; } if (left < this.padding) { left = this.padding; } this.frame.style.left = left + 'px'; this.frame.style.top = top + 'px'; this.frame.style.visibility = 'visible'; this.hidden = false; } else { this.hide(); } } }, { key: 'hide', /** * Hide the popup window */ value: function hide() { this.hidden = true; this.frame.style.visibility = 'hidden'; } }]); return Popup; })(); exports['default'] = Popup; module.exports = exports['default']; /***/ }, /* 105 */ /***/ function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Node = __webpack_require__(62); var Edge = __webpack_require__(82); var util = __webpack_require__(1); var SelectionHandler = (function () { function SelectionHandler(body, canvas) { var _this = this; _classCallCheck(this, SelectionHandler); this.body = body; this.canvas = canvas; this.selectionObj = { nodes: [], edges: [] }; this.hoverObj = { nodes: {}, edges: {} }; this.options = {}; this.defaultOptions = { multiselect: false, selectable: true, selectConnectedEdges: true, hoverConnectedEdges: true }; util.extend(this.options, this.defaultOptions); this.body.emitter.on("_dataChanged", function () { _this.updateSelection(); }); } _createClass(SelectionHandler, [{ key: "setOptions", value: function setOptions(options) { if (options !== undefined) { var fields = ["multiselect", "hoverConnectedEdges", "selectable", "selectConnectedEdges"]; util.selectiveDeepExtend(fields, this.options, options); } } }, { key: "selectOnPoint", /** * handles the selection part of the tap; * * @param {Object} pointer * @private */ value: function selectOnPoint(pointer) { var selected = false; if (this.options.selectable === true) { var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); // unselect after getting the objects in order to restore width and height. this.unselectAll(); if (obj !== undefined) { selected = this.selectObject(obj); } this.body.emitter.emit("_requestRedraw"); } return selected; } }, { key: "selectAdditionalOnPoint", value: function selectAdditionalOnPoint(pointer) { var selectionChanged = false; if (this.options.selectable === true) { var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); if (obj !== undefined) { selectionChanged = true; if (obj.isSelected() === true) { this.deselectObject(obj); } else { this.selectObject(obj); } this.body.emitter.emit("_requestRedraw"); } } return selectionChanged; } }, { key: "_generateClickEvent", value: function _generateClickEvent(eventType, event, pointer, oldSelection) { var emptySelection = arguments[4] === undefined ? false : arguments[4]; var properties = undefined; if (emptySelection === true) { properties = { nodes: [], edges: [] }; } else { properties = this.getSelection(); } properties["pointer"] = { DOM: { x: pointer.x, y: pointer.y }, canvas: this.canvas.DOMtoCanvas(pointer) }; properties["event"] = event; if (oldSelection !== undefined) { properties["previousSelection"] = oldSelection; } this.body.emitter.emit(eventType, properties); } }, { key: "selectObject", value: function selectObject(obj) { var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; if (obj !== undefined) { if (obj instanceof Node) { if (highlightEdges === true) { this._selectConnectedEdges(obj); } } obj.select(); this._addToSelection(obj); return true; } return false; } }, { key: "deselectObject", value: function deselectObject(obj) { if (obj.isSelected() === true) { obj.selected = false; this._removeFromSelection(obj); } } }, { key: "_getAllNodesOverlappingWith", /** * retrieve all nodes overlapping with given object * @param {Object} object An object with parameters left, top, right, bottom * @return {Number[]} An array with id's of the overlapping nodes * @private */ value: function _getAllNodesOverlappingWith(object) { var overlappingNodes = []; var nodes = this.body.nodes; for (var i = 0; i < this.body.nodeIndices.length; i++) { var nodeId = this.body.nodeIndices[i]; if (nodes[nodeId].isOverlappingWith(object)) { overlappingNodes.push(nodeId); } } return overlappingNodes; } }, { key: "_pointerToPositionObject", /** * Return a position object in canvasspace from a single point in screenspace * * @param pointer * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ value: function _pointerToPositionObject(pointer) { var canvasPos = this.canvas.DOMtoCanvas(pointer); return { left: canvasPos.x - 1, top: canvasPos.y + 1, right: canvasPos.x + 1, bottom: canvasPos.y - 1 }; } }, { key: "getNodeAt", /** * Get the top node at the a specific point (like a click) * * @param {{x: Number, y: Number}} pointer * @return {Node | undefined} node * @private */ value: function getNodeAt(pointer) { var returnNode = arguments[1] === undefined ? true : arguments[1]; // we first check if this is an navigation controls element var positionObject = this._pointerToPositionObject(pointer); var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); // if there are overlapping nodes, select the last one, this is the // one which is drawn on top of the others if (overlappingNodes.length > 0) { if (returnNode === true) { return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; } else { return overlappingNodes[overlappingNodes.length - 1]; } } else { return undefined; } } }, { key: "_getEdgesOverlappingWith", /** * retrieve all edges overlapping with given object, selector is around center * @param {Object} object An object with parameters left, top, right, bottom * @return {Number[]} An array with id's of the overlapping nodes * @private */ value: function _getEdgesOverlappingWith(object, overlappingEdges) { var edges = this.body.edges; for (var i = 0; i < this.body.edgeIndices.length; i++) { var edgeId = this.body.edgeIndices[i]; if (edges[edgeId].isOverlappingWith(object)) { overlappingEdges.push(edgeId); } } } }, { key: "_getAllEdgesOverlappingWith", /** * retrieve all nodes overlapping with given object * @param {Object} object An object with parameters left, top, right, bottom * @return {Number[]} An array with id's of the overlapping nodes * @private */ value: function _getAllEdgesOverlappingWith(object) { var overlappingEdges = []; this._getEdgesOverlappingWith(object, overlappingEdges); return overlappingEdges; } }, { key: "getEdgeAt", /** * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * * @param pointer * @returns {undefined} * @private */ value: function getEdgeAt(pointer) { var returnEdge = arguments[1] === undefined ? true : arguments[1]; var positionObject = this._pointerToPositionObject(pointer); var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); if (overlappingEdges.length > 0) { if (returnEdge === true) { return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; } else { return overlappingEdges[overlappingEdges.length - 1]; } } else { return undefined; } } }, { key: "_addToSelection", /** * Add object to the selection array. * * @param obj * @private */ value: function _addToSelection(obj) { if (obj instanceof Node) { this.selectionObj.nodes[obj.id] = obj; } else { this.selectionObj.edges[obj.id] = obj; } } }, { key: "_addToHover", /** * Add object to the selection array. * * @param obj * @private */ value: function _addToHover(obj) { if (obj instanceof Node) { this.hoverObj.nodes[obj.id] = obj; } else { this.hoverObj.edges[obj.id] = obj; } } }, { key: "_removeFromSelection", /** * Remove a single option from selection. * * @param {Object} obj * @private */ value: function _removeFromSelection(obj) { if (obj instanceof Node) { delete this.selectionObj.nodes[obj.id]; } else { delete this.selectionObj.edges[obj.id]; } } }, { key: "unselectAll", /** * Unselect all. The selectionObj is useful for this. * * @private */ value: function unselectAll() { for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { this.selectionObj.nodes[nodeId].unselect(); } } for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { this.selectionObj.edges[edgeId].unselect(); } } this.selectionObj = { nodes: {}, edges: {} }; } }, { key: "_getSelectedNodeCount", /** * return the number of selected nodes * * @returns {number} * @private */ value: function _getSelectedNodeCount() { var count = 0; for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { count += 1; } } return count; } }, { key: "_getSelectedNode", /** * return the selected node * * @returns {number} * @private */ value: function _getSelectedNode() { for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { return this.selectionObj.nodes[nodeId]; } } return undefined; } }, { key: "_getSelectedEdge", /** * return the selected edge * * @returns {number} * @private */ value: function _getSelectedEdge() { for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { return this.selectionObj.edges[edgeId]; } } return undefined; } }, { key: "_getSelectedEdgeCount", /** * return the number of selected edges * * @returns {number} * @private */ value: function _getSelectedEdgeCount() { var count = 0; for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { count += 1; } } return count; } }, { key: "_getSelectedObjectCount", /** * return the number of selected objects. * * @returns {number} * @private */ value: function _getSelectedObjectCount() { var count = 0; for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { count += 1; } } for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { count += 1; } } return count; } }, { key: "_selectionIsEmpty", /** * Check if anything is selected * * @returns {boolean} * @private */ value: function _selectionIsEmpty() { for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { return false; } } for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { return false; } } return true; } }, { key: "_clusterInSelection", /** * check if one of the selected nodes is a cluster. * * @returns {boolean} * @private */ value: function _clusterInSelection() { for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { if (this.selectionObj.nodes[nodeId].clusterSize > 1) { return true; } } } return false; } }, { key: "_selectConnectedEdges", /** * select the edges connected to the node that is being selected * * @param {Node} node * @private */ value: function _selectConnectedEdges(node) { for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; edge.select(); this._addToSelection(edge); } } }, { key: "_hoverConnectedEdges", /** * select the edges connected to the node that is being selected * * @param {Node} node * @private */ value: function _hoverConnectedEdges(node) { for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; edge.hover = true; this._addToHover(edge); } } }, { key: "_unselectConnectedEdges", /** * unselect the edges connected to the node that is being selected * * @param {Node} node * @private */ value: function _unselectConnectedEdges(node) { for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; edge.unselect(); this._removeFromSelection(edge); } } }, { key: "blurObject", /** * This is called when someone clicks on a node. either select or deselect it. * If there is an existing selection and we don't want to append to it, clear the existing selection * * @param {Node || Edge} object * @private */ value: function blurObject(object) { if (object.hover === true) { object.hover = false; this.body.emitter.emit("blurNode", { node: object.id }); } } }, { key: "hoverObject", /** * This is called when someone clicks on a node. either select or deselect it. * If there is an existing selection and we don't want to append to it, clear the existing selection * * @param {Node || Edge} object * @private */ value: function hoverObject(object) { var hoverChanged = false; // remove all node hover highlights for (var nodeId in this.hoverObj.nodes) { if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { if (object === undefined) { this.blurObject(this.hoverObj.nodes[nodeId]); hoverChanged = true; } else if (object instanceof Node && object.id != nodeId || object instanceof Edge || object === undefined) { this.blurObject(this.hoverObj.nodes[nodeId]); hoverChanged = true; delete this.hoverObj.nodes[nodeId]; } } } // removing all edge hover highlights for (var edgeId in this.hoverObj.edges) { if (this.hoverObj.edges.hasOwnProperty(edgeId)) { this.hoverObj.edges[edgeId].hover = false; delete this.hoverObj.edges[edgeId]; } } if (object !== undefined) { if (object.hover === false) { object.hover = true; this._addToHover(object); hoverChanged = true; if (object instanceof Node) { this.body.emitter.emit("hoverNode", { node: object.id }); } } if (object instanceof Node && this.options.hoverConnectedEdges === true) { this._hoverConnectedEdges(object); } } if (hoverChanged === true) { this.body.emitter.emit("_requestRedraw"); } } }, { key: "getSelection", /** * * retrieve the currently selected objects * @return {{nodes: Array., edges: Array.}} selection */ value: function getSelection() { var nodeIds = this.getSelectedNodes(); var edgeIds = this.getSelectedEdges(); return { nodes: nodeIds, edges: edgeIds }; } }, { key: "getSelectedNodes", /** * * retrieve the currently selected nodes * @return {String[]} selection An array with the ids of the * selected nodes. */ value: function getSelectedNodes() { var idArray = []; if (this.options.selectable === true) { for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { idArray.push(nodeId); } } } return idArray; } }, { key: "getSelectedEdges", /** * * retrieve the currently selected edges * @return {Array} selection An array with the ids of the * selected nodes. */ value: function getSelectedEdges() { var idArray = []; if (this.options.selectable === true) { for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { idArray.push(edgeId); } } } return idArray; } }, { key: "selectNodes", /** * select zero or more nodes with the option to highlight edges * @param {Number[] | String[]} selection An array with the ids of the * selected nodes. * @param {boolean} [highlightEdges] */ value: function selectNodes(selection) { var highlightEdges = arguments[1] === undefined ? true : arguments[1]; var i = undefined, id = undefined; if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; // first unselect any selected node this.unselectAll(); for (i = 0; i < selection.length; i++) { id = selection[i]; var node = this.body.nodes[id]; if (!node) { throw new RangeError("Node with id \"" + id + "\" not found"); } this.selectObject(node, highlightEdges); } this.body.emitter.emit("_requestRedraw"); } }, { key: "selectEdges", /** * select zero or more edges * @param {Number[] | String[]} selection An array with the ids of the * selected nodes. */ value: function selectEdges(selection) { var i = undefined, id = undefined; if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; // first unselect any selected objects this.unselectAll(); for (i = 0; i < selection.length; i++) { id = selection[i]; var edge = this.body.edges[id]; if (!edge) { throw new RangeError("Edge with id \"" + id + "\" not found"); } this.selectObject(edge); } this.body.emitter.emit("_requestRedraw"); } }, { key: "updateSelection", /** * Validate the selection: remove ids of nodes which no longer exist * @private */ value: function updateSelection() { for (var nodeId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { if (!this.body.nodes.hasOwnProperty(nodeId)) { delete this.selectionObj.nodes[nodeId]; } } } for (var edgeId in this.selectionObj.edges) { if (this.selectionObj.edges.hasOwnProperty(edgeId)) { if (!this.body.edges.hasOwnProperty(edgeId)) { delete this.selectionObj.edges[edgeId]; } } } } }]); return SelectionHandler; })(); exports["default"] = SelectionHandler; module.exports = exports["default"]; /***/ }, /* 106 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); var LayoutEngine = (function () { function LayoutEngine(body) { _classCallCheck(this, LayoutEngine); this.body = body; this.initialRandomSeed = Math.round(Math.random() * 1000000); this.randomSeed = this.initialRandomSeed; this.options = {}; this.optionsBackup = {}; this.defaultOptions = { randomSeed: undefined, hierarchical: { enabled: false, levelSeparation: 150, direction: 'UD', // UD, DU, LR, RL sortMethod: 'hubsize' // hubsize, directed } }; util.extend(this.options, this.defaultOptions); this.hierarchicalLevels = {}; this.bindEventListeners(); } _createClass(LayoutEngine, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; this.body.emitter.on('_dataChanged', function () { _this.setupHierarchicalLayout(); }); this.body.emitter.on('_resetHierarchicalLayout', function () { _this.setupHierarchicalLayout(); }); } }, { key: 'setOptions', value: function setOptions(options, allOptions) { if (options !== undefined) { var prevHierarchicalState = this.options.hierarchical.enabled; util.mergeOptions(this.options, options, 'hierarchical'); if (options.randomSeed !== undefined) { this.initialRandomSeed = options.randomSeed; } if (this.options.hierarchical.enabled === true) { // make sure the level seperation is the right way up if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') { if (this.options.hierarchical.levelSeparation > 0) { this.options.hierarchical.levelSeparation *= -1; } } else { if (this.options.hierarchical.levelSeparation < 0) { this.options.hierarchical.levelSeparation *= -1; } } this.body.emitter.emit('_resetHierarchicalLayout'); // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. return this.adaptAllOptions(allOptions); } else { if (prevHierarchicalState === true) { // refresh the overridden options for nodes and edges. this.body.emitter.emit('refresh'); return util.deepExtend(allOptions, this.optionsBackup); } } } return allOptions; } }, { key: 'adaptAllOptions', value: function adaptAllOptions(allOptions) { if (this.options.hierarchical.enabled === true) { // set the physics if (allOptions.physics === undefined || allOptions.physics === true) { allOptions.physics = { solver: 'hierarchicalRepulsion' }; this.optionsBackup.physics = { solver: 'barnesHut' }; } else if (typeof allOptions.physics === 'object') { this.optionsBackup.physics = { solver: 'barnesHut' }; if (allOptions.physics.solver !== undefined) { this.optionsBackup.physics = { solver: allOptions.physics.solver }; } allOptions.physics['solver'] = 'hierarchicalRepulsion'; } else if (allOptions.physics !== false) { this.optionsBackup.physics = { solver: 'barnesHut' }; allOptions.physics['solver'] = 'hierarchicalRepulsion'; } // get the type of static smooth curve in case it is required var type = 'horizontal'; if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') { type = 'vertical'; } // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. if (allOptions.edges === undefined) { this.optionsBackup.edges = { smooth: { enabled: true, type: 'dynamic' } }; allOptions.edges = { smooth: false }; } else if (allOptions.edges.smooth === undefined) { this.optionsBackup.edges = { smooth: { enabled: true, type: 'dynamic' } }; allOptions.edges.smooth = false; } else { if (typeof allOptions.edges.smooth === 'boolean') { this.optionsBackup.edges = { smooth: allOptions.edges.smooth }; allOptions.edges.smooth = { enabled: allOptions.edges.smooth, type: type }; } else { // allow custom types except for dynamic if (allOptions.edges.smooth.type !== undefined && allOptions.edges.smooth.type !== 'dynamic') { type = allOptions.edges.smooth.type; } this.optionsBackup.edges = { smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, type: allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type, roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness }; allOptions.edges.smooth = { enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, type: type, roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness }; } } // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth. this.body.emitter.emit('_forceDisableDynamicCurves', type); } return allOptions; } }, { key: 'seededRandom', value: function seededRandom() { var x = Math.sin(this.randomSeed++) * 10000; return x - Math.floor(x); } }, { key: 'positionInitially', value: function positionInitially(nodesArray) { if (this.options.hierarchical.enabled !== true) { this.randomSeed = this.initialRandomSeed; for (var i = 0; i < nodesArray.length; i++) { var node = nodesArray[i]; if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { var radius = 10 * 0.1 * nodesArray.length + 10; var angle = 2 * Math.PI * this.seededRandom(); if (node.options.fixed.x === false) { node.x = radius * Math.cos(angle); } if (node.options.fixed.x === false) { node.y = radius * Math.sin(angle); } } } } } }, { key: 'getSeed', value: function getSeed() { return this.initialRandomSeed; } }, { key: 'setupHierarchicalLayout', /** * This is the main function to layout the nodes in a hierarchical way. * It checks if the node details are supplied correctly * * @private */ value: function setupHierarchicalLayout() { if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { // get the size of the largest hubs and check if the user has defined a level for a node. var node = undefined, nodeId = undefined; var definedLevel = false; var undefinedLevel = false; this.hierarchicalLevels = {}; this.nodeSpacing = 100; for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; if (node.options.level !== undefined) { definedLevel = true; this.hierarchicalLevels[nodeId] = node.options.level; } else { undefinedLevel = true; } } } // if the user defined some levels but not all, alert and run without hierarchical layout if (undefinedLevel === true && definedLevel === true) { throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.'); return; } else { // setup the system to use hierarchical method. //this._changeConstants(); // define levels if undefined by the users. Based on hubsize if (undefinedLevel === true) { if (this.options.hierarchical.sortMethod === 'hubsize') { this._determineLevelsByHubsize(); } else if (this.options.hierarchical.sortMethod === 'directed' || 'direction') { this._determineLevelsDirected(); } } // check the distribution of the nodes per level. var distribution = this._getDistribution(); // place the nodes on the canvas. this._placeNodesByHierarchy(distribution); } } } }, { key: '_placeNodesByHierarchy', /** * This function places the nodes on the canvas based on the hierarchial distribution. * * @param {Object} distribution | obtained by the function this._getDistribution() * @private */ value: function _placeNodesByHierarchy(distribution) { var nodeId = undefined, node = undefined; this.positionedNodes = {}; // start placing all the level 0 nodes first. Then recursively position their branches. for (var level in distribution) { if (distribution.hasOwnProperty(level)) { for (nodeId in distribution[level].nodes) { if (distribution[level].nodes.hasOwnProperty(nodeId)) { node = distribution[level].nodes[nodeId]; if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { if (node.x === undefined) { node.x = distribution[level].distance; } distribution[level].distance = node.x + this.nodeSpacing; } else { if (node.y === undefined) { node.y = distribution[level].distance; } distribution[level].distance = node.y + this.nodeSpacing; } this.positionedNodes[nodeId] = true; this._placeBranchNodes(node.edges, node.id, distribution, level); } } } } } }, { key: '_getDistribution', /** * This function get the distribution of levels based on hubsize * * @returns {Object} * @private */ value: function _getDistribution() { var distribution = {}; var nodeId = undefined, node = undefined; // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. // the fix of X is removed after the x value has been set. for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; var level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { node.y = this.options.hierarchical.levelSeparation * level; node.options.fixed.y = true; } else { node.x = this.options.hierarchical.levelSeparation * level; node.options.fixed.x = true; } if (distribution[level] === undefined) { distribution[level] = { amount: 0, nodes: {}, distance: 0 }; } distribution[level].amount += 1; distribution[level].nodes[nodeId] = node; } } return distribution; } }, { key: '_getHubSize', /** * Get the hubsize from all remaining unlevelled nodes. * * @returns {number} * @private */ value: function _getHubSize() { var hubSize = 0; for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { var node = this.body.nodes[nodeId]; if (this.hierarchicalLevels[nodeId] === undefined) { hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; } } } return hubSize; } }, { key: '_determineLevelsByHubsize', /** * this function allocates nodes in levels based on the recursive branching from the largest hubs. * * @param hubsize * @private */ value: function _determineLevelsByHubsize() { var nodeId = undefined, node = undefined; var hubSize = 1; while (hubSize > 0) { // determine hubs hubSize = this._getHubSize(); if (hubSize === 0) break; for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; if (node.edges.length === hubSize) { this._setLevelByHubsize(0, node); } } } } } }, { key: '_setLevelByHubsize', /** * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. * * @param level * @param edges * @param parentId * @private */ value: function _setLevelByHubsize(level, node) { if (this.hierarchicalLevels[node.id] !== undefined) return; var childNode = undefined; this.hierarchicalLevels[node.id] = level; for (var i = 0; i < node.edges.length; i++) { if (node.edges[i].toId === node.id) { childNode = node.edges[i].from; } else { childNode = node.edges[i].to; } this._setLevelByHubsize(level + 1, childNode); } } }, { key: '_determineLevelsDirected', /** * this function allocates nodes in levels based on the direction of the edges * * @param hubsize * @private */ value: function _determineLevelsDirected() { var nodeId = undefined, node = undefined; var minLevel = 10000; // set first node to source for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; this._setLevelDirected(minLevel, node); } } // get the minimum level for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; } } // subtract the minimum from the set so we have a range starting from 0 for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { this.hierarchicalLevels[nodeId] -= minLevel; } } } }, { key: '_setLevelDirected', /** * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * * @param level * @param edges * @param parentId * @private */ value: function _setLevelDirected(level, node) { if (this.hierarchicalLevels[node.id] !== undefined) return; var childNode = undefined; this.hierarchicalLevels[node.id] = level; for (var i = 0; i < node.edges.length; i++) { if (node.edges[i].toId === node.id) { childNode = node.edges[i].from; this._setLevelDirected(level - 1, childNode); } else { childNode = node.edges[i].to; this._setLevelDirected(level + 1, childNode); } } } }, { key: '_placeBranchNodes', /** * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes * on a X position that ensures there will be no overlap. * * @param edges * @param parentId * @param distribution * @param parentLevel * @private */ value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { for (var i = 0; i < edges.length; i++) { var childNode = undefined; var parentNode = undefined; if (edges[i].toId === parentId) { childNode = edges[i].from; parentNode = edges[i].to; } else { childNode = edges[i].to; parentNode = edges[i].from; } var childNodeLevel = this.hierarchicalLevels[childNode.id]; if (this.positionedNodes[childNode.id] === undefined) { // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. if (childNodeLevel > parentLevel) { if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { if (childNode.x === undefined) { childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); } distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; this.positionedNodes[childNode.id] = true; } else { if (childNode.y === undefined) { childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); } distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; } this.positionedNodes[childNode.id] = true; if (childNode.edges.length > 1) { this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); } } } } } }]); return LayoutEngine; })(); exports['default'] = LayoutEngine; module.exports = exports['default']; /***/ }, /* 107 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); var Hammer = __webpack_require__(23); var hammerUtil = __webpack_require__(28); /** * clears the toolbar div element of children * * @private */ var ManipulationSystem = (function () { function ManipulationSystem(body, canvas, selectionHandler) { var _this = this; _classCallCheck(this, ManipulationSystem); this.body = body; this.canvas = canvas; this.selectionHandler = selectionHandler; this.editMode = false; this.manipulationDiv = undefined; this.editModeDiv = undefined; this.closeDiv = undefined; this.manipulationHammers = []; this.temporaryUIFunctions = {}; this.temporaryEventFunctions = []; this.touchTime = 0; this.temporaryIds = { nodes: [], edges: [] }; this.guiEnabled = false; this.inMode = false; this.selectedControlNode = undefined; this.options = {}; this.defaultOptions = { enabled: false, initiallyActive: false, addNode: true, addEdge: true, editNode: undefined, editEdge: true, deleteNode: true, deleteEdge: true, controlNodeStyle: { shape: 'dot', size: 6, color: { background: '#ff0000', border: '#3c3c3c', highlight: { background: '#07f968', border: '#3c3c3c' } }, borderWidth: 2, borderWidthSelected: 2 } }; util.extend(this.options, this.defaultOptions); this.body.emitter.on('destroy', function () { _this._clean(); }); this.body.emitter.on('_dataChanged', this._restore.bind(this)); this.body.emitter.on('_resetData', this._restore.bind(this)); } _createClass(ManipulationSystem, [{ key: '_restore', /** * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. * @private */ value: function _restore() { if (this.inMode !== false) { if (this.options.initiallyActive === true) { this.enableEditMode(); } else { this.disableEditMode(); } } } }, { key: 'setOptions', /** * Set the Options * @param options */ value: function setOptions(options, allOptions, globalOptions) { if (allOptions !== undefined) { if (allOptions.locale !== undefined) { this.options.locale = allOptions.locale; } else { this.options.locale = globalOptions.locale; } if (allOptions.locales !== undefined) { this.options.locales = allOptions.locales; } else { this.options.locales = globalOptions.locales; } } if (options !== undefined) { if (typeof options === 'boolean') { this.options.enabled = options; } else { this.options.enabled = true; util.deepExtend(this.options, options); } if (this.options.initiallyActive === true) { this.editMode = true; } this._setup(); } } }, { key: 'toggleEditMode', /** * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. * * @private */ value: function toggleEditMode() { if (this.editMode === true) { this.disableEditMode(); } else { this.enableEditMode(); } } }, { key: 'enableEditMode', value: function enableEditMode() { this.editMode = true; this._clean(); if (this.guiEnabled === true) { this.manipulationDiv.style.display = 'block'; this.closeDiv.style.display = 'block'; this.editModeDiv.style.display = 'none'; this.showManipulatorToolbar(); } } }, { key: 'disableEditMode', value: function disableEditMode() { this.editMode = false; this._clean(); if (this.guiEnabled === true) { this.manipulationDiv.style.display = 'none'; this.closeDiv.style.display = 'none'; this.editModeDiv.style.display = 'block'; this._createEditButton(); } } }, { key: 'showManipulatorToolbar', /** * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. * * @private */ value: function showManipulatorToolbar() { // restore the state of any bound functions or events, remove control nodes, restore physics this._clean(); // reset global letiables this.manipulationDOM = {}; // if the gui is enabled, draw all elements. if (this.guiEnabled === true) { // a _restore will hide these menus this.editMode = true; this.manipulationDiv.style.display = 'block'; this.closeDiv.style.display = 'block'; var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); var selectedTotalCount = selectedNodeCount + selectedEdgeCount; var locale = this.options.locales[this.options.locale]; var needSeperator = false; if (this.options.addNode !== false) { this._createAddNodeButton(locale); needSeperator = true; } if (this.options.addEdge !== false) { if (needSeperator === true) { this._createSeperator(1); } else { needSeperator = true; } this._createAddEdgeButton(locale); } if (selectedNodeCount === 1 && typeof this.options.editNode === 'function') { if (needSeperator === true) { this._createSeperator(2); } else { needSeperator = true; } this._createEditNodeButton(locale); } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.editEdge !== false) { if (needSeperator === true) { this._createSeperator(3); } else { needSeperator = true; } this._createEditEdgeButton(locale); } // remove buttons if (selectedTotalCount !== 0) { if (selectedNodeCount === 1 && this.options.deleteNode !== false) { if (needSeperator === true) { this._createSeperator(4); } this._createDeleteButton(locale); } else if (selectedNodeCount === 0 && this.options.deleteEdge !== false) { if (needSeperator === true) { this._createSeperator(4); } this._createDeleteButton(locale); } } // bind the close button this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); // refresh this bar based on what has been selected this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); } // redraw to show any possible changes this.body.emitter.emit('_redraw'); } }, { key: 'addNodeMode', /** * Create the toolbar for adding Nodes * * @private */ value: function addNodeMode() { // when using the gui, enable edit mode if it wasnt already. if (this.editMode !== true) { this.enableEditMode(); } // restore the state of any bound functions or events, remove control nodes, restore physics this._clean(); this.inMode = 'addNode'; if (this.guiEnabled === true) { var locale = this.options.locales[this.options.locale]; this.manipulationDOM = {}; this._createBackButton(locale); this._createSeperator(); this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']); // bind the close button this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } this._temporaryBindEvent('click', this._performAddNode.bind(this)); } }, { key: 'editNode', /** * call the bound function to handle the editing of the node. The node has to be selected. * * @private */ value: function editNode() { var _this2 = this; // when using the gui, enable edit mode if it wasnt already. if (this.editMode !== true) { this.enableEditMode(); } // restore the state of any bound functions or events, remove control nodes, restore physics this._clean(); var node = this.selectionHandler._getSelectedNode(); if (node !== undefined) { this.inMode = 'editNode'; if (typeof this.options.editNode === 'function') { if (node.isCluster !== true) { var data = util.deepExtend({}, node.options, true); data.x = node.x; data.y = node.y; if (this.options.editNode.length === 2) { this.options.editNode(data, function (finalizedData) { if (finalizedData !== null && finalizedData !== undefined && _this2.inMode === 'editNode') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { _this2.body.data.nodes.getDataSet().update(finalizedData); } _this2.showManipulatorToolbar(); }); } else { throw new Error('The function for edit does not support two arguments (data, callback)'); } } else { alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); } } else { throw new Error('No function has been configured to handle the editing of nodes.'); } } else { this.showManipulatorToolbar(); } } }, { key: 'addEdgeMode', /** * create the toolbar to connect nodes * * @private */ value: function addEdgeMode() { // when using the gui, enable edit mode if it wasnt already. if (this.editMode !== true) { this.enableEditMode(); } // restore the state of any bound functions or events, remove control nodes, restore physics this._clean(); this.inMode = 'addEdge'; if (this.guiEnabled === true) { var locale = this.options.locales[this.options.locale]; this.manipulationDOM = {}; this._createBackButton(locale); this._createSeperator(); this._createDescription(locale['edgeDescription'] || this.options.locales['en']['edgeDescription']); // bind the close button this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } // temporarily overload functions this._temporaryBindUI('onTouch', this._handleConnect.bind(this)); this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this)); this._temporaryBindUI('onDrag', this._dragControlNode.bind(this)); this._temporaryBindUI('onRelease', this._finishConnect.bind(this)); this._temporaryBindUI('onDragStart', function () {}); this._temporaryBindUI('onHold', function () {}); } }, { key: 'editEdgeMode', /** * create the toolbar to edit edges * * @private */ value: function editEdgeMode() { var _this3 = this; // when using the gui, enable edit mode if it wasnt already. if (this.editMode !== true) { this.enableEditMode(); } // restore the state of any bound functions or events, remove control nodes, restore physics this._clean(); this.inMode = 'editEdge'; if (this.guiEnabled === true) { var locale = this.options.locales[this.options.locale]; this.manipulationDOM = {}; this._createBackButton(locale); this._createSeperator(); this._createDescription(locale['editEdgeDescription'] || this.options.locales['en']['editEdgeDescription']); // bind the close button this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; if (this.edgeBeingEditedId !== undefined) { (function () { var edge = _this3.body.edges[_this3.edgeBeingEditedId]; // create control nodes var controlNodeFrom = _this3._getNewTargetNode(edge.from.x, edge.from.y); var controlNodeTo = _this3._getNewTargetNode(edge.to.x, edge.to.y); _this3.temporaryIds.nodes.push(controlNodeFrom.id); _this3.temporaryIds.nodes.push(controlNodeTo.id); _this3.body.nodes[controlNodeFrom.id] = controlNodeFrom; _this3.body.nodeIndices.push(controlNodeFrom.id); _this3.body.nodes[controlNodeTo.id] = controlNodeTo; _this3.body.nodeIndices.push(controlNodeTo.id); // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI _this3._temporaryBindUI('onTouch', _this3._controlNodeTouch.bind(_this3)); // used to get the position _this3._temporaryBindUI('onTap', function () {}); // disabled _this3._temporaryBindUI('onHold', function () {}); // disabled _this3._temporaryBindUI('onDragStart', _this3._controlNodeDragStart.bind(_this3)); // used to select control node _this3._temporaryBindUI('onDrag', _this3._controlNodeDrag.bind(_this3)); // used to drag control node _this3._temporaryBindUI('onDragEnd', _this3._controlNodeDragEnd.bind(_this3)); // used to connect or revert control nodes _this3._temporaryBindUI('onMouseMove', function () {}); // disabled // create function to position control nodes correctly on movement // automatically cleaned up because we use the temporary bind _this3._temporaryBindEvent('beforeDrawing', function (ctx) { var positions = edge.edgeType.findBorderPositions(ctx); if (controlNodeFrom.selected === false) { controlNodeFrom.x = positions.from.x; controlNodeFrom.y = positions.from.y; } if (controlNodeTo.selected === false) { controlNodeTo.x = positions.to.x; controlNodeTo.y = positions.to.y; } }); _this3.body.emitter.emit('_redraw'); })(); } else { this.showManipulatorToolbar(); } } }, { key: 'deleteSelected', /** * delete everything in the selection * * @private */ value: function deleteSelected() { var _this4 = this; // when using the gui, enable edit mode if it wasnt already. if (this.editMode !== true) { this.enableEditMode(); } // restore the state of any bound functions or events, remove control nodes, restore physics this._clean(); this.inMode = 'delete'; var selectedNodes = this.selectionHandler.getSelectedNodes(); var selectedEdges = this.selectionHandler.getSelectedEdges(); var deleteFunction = undefined; if (selectedNodes.length > 0) { for (var i = 0; i < selectedNodes.length; i++) { if (this.body.nodes[selectedNodes[i]].isCluster === true) { alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']); return; } } if (typeof this.options.deleteNode === 'function') { deleteFunction = this.options.deleteNode; } } else if (selectedEdges.length > 0) { if (typeof this.options.deleteEdge === 'function') { deleteFunction = this.options.deleteEdge; } } if (typeof deleteFunction === 'function') { var data = { nodes: selectedNodes, edges: selectedEdges }; if (deleteFunction.length === 2) { deleteFunction(data, function (finalizedData) { if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'delete') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { _this4.body.data.edges.getDataSet().remove(finalizedData.edges); _this4.body.data.nodes.getDataSet().remove(finalizedData.nodes); _this4.body.emitter.emit('startSimulation'); _this4.showManipulatorToolbar(); } }); } else { throw new Error('The function for delete does not support two arguments (data, callback)'); } } else { this.body.data.edges.getDataSet().remove(selectedEdges); this.body.data.nodes.getDataSet().remove(selectedNodes); this.body.emitter.emit('startSimulation'); this.showManipulatorToolbar(); } } }, { key: '_setup', //********************************************** PRIVATE ***************************************// /** * draw or remove the DOM * @private */ value: function _setup() { if (this.options.enabled === true) { // Enable the GUI this.guiEnabled = true; this._createWrappers(); if (this.editMode === false) { this._createEditButton(); } else { this.showManipulatorToolbar(); } } else { this._removeManipulationDOM(); // disable the gui this.guiEnabled = false; } } }, { key: '_createWrappers', /** * create the div overlays that contain the DOM * @private */ value: function _createWrappers() { // load the manipulator HTML elements. All styling done in css. if (this.manipulationDiv === undefined) { this.manipulationDiv = document.createElement('div'); this.manipulationDiv.className = 'vis-manipulation'; if (this.editMode === true) { this.manipulationDiv.style.display = 'block'; } else { this.manipulationDiv.style.display = 'none'; } this.canvas.frame.appendChild(this.manipulationDiv); } // container for the edit button. if (this.editModeDiv === undefined) { this.editModeDiv = document.createElement('div'); this.editModeDiv.className = 'vis-edit-mode'; if (this.editMode === true) { this.editModeDiv.style.display = 'none'; } else { this.editModeDiv.style.display = 'block'; } this.editModeDiv.appendChild(this._createIcon('fa-pencil')) this.canvas.frame.appendChild(this.editModeDiv); } // container for the close div button if (this.closeDiv === undefined) { this.closeDiv = document.createElement('div'); this.closeDiv.className = 'vis-close'; this.closeDiv.style.display = this.manipulationDiv.style.display; this.closeDiv.appendChild(this._createIcon('fa-times')) this.canvas.frame.appendChild(this.closeDiv); } } }, { key: '_getNewTargetNode', /** * generate a new target node. Used for creating new edges and editing edges * @param x * @param y * @returns {*} * @private */ value: function _getNewTargetNode(x, y) { var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); controlNodeStyle.id = 'targetNode' + util.randomUUID(); controlNodeStyle.hidden = false; controlNodeStyle.physics = false; controlNodeStyle.x = x; controlNodeStyle.y = y; return this.body.functions.createNode(controlNodeStyle); } }, { key: '_createEditButton', /** * Create the edit button */ value: function _createEditButton() { // restore everything to it's original state (if applicable) this._clean(); // reset the manipulationDOM this.manipulationDOM = {}; // empty the editModeDiv util.recursiveDOMDelete(this.editModeDiv); // create the contents for the editMode button var locale = this.options.locales[this.options.locale]; var button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit'], 'fa-pencil'); this.editModeDiv.appendChild(button); // bind a hammer listener to the button, calling the function toggleEditMode. this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); } }, { key: '_clean', /** * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. * @private */ value: function _clean() { // not in mode this.inMode = false; // _clean the divs if (this.guiEnabled === true) { util.recursiveDOMDelete(this.editModeDiv); util.recursiveDOMDelete(this.manipulationDiv); // removes all the bindings and overloads this._cleanManipulatorHammers(); } // remove temporary nodes and edges this._cleanupTemporaryNodesAndEdges(); // restore overloaded UI functions this._unbindTemporaryUIs(); // remove the temporaryEventFunctions this._unbindTemporaryEvents(); // restore the physics if required this.body.emitter.emit('restorePhysics'); } }, { key: '_cleanManipulatorHammers', /** * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. * @private */ value: function _cleanManipulatorHammers() { // _clean hammer bindings if (this.manipulationHammers.length != 0) { for (var i = 0; i < this.manipulationHammers.length; i++) { this.manipulationHammers[i].destroy(); } this.manipulationHammers = []; } } }, { key: '_removeManipulationDOM', /** * Remove all DOM elements created by this module. * @private */ value: function _removeManipulationDOM() { // removes all the bindings and overloads this._clean(); // empty the manipulation divs util.recursiveDOMDelete(this.manipulationDiv); util.recursiveDOMDelete(this.editModeDiv); util.recursiveDOMDelete(this.closeDiv); // remove the manipulation divs if (this.manipulationDiv) { this.canvas.frame.removeChild(this.manipulationDiv); } if (this.editModeDiv) { this.canvas.frame.removeChild(this.editModeDiv); } if (this.closeDiv) { this.canvas.frame.removeChild(this.manipulationDiv); } // set the references to undefined this.manipulationDiv = undefined; this.editModeDiv = undefined; this.closeDiv = undefined; } }, { key: '_createSeperator', /** * create a seperator line. the index is to differentiate in the manipulation dom * @param index * @private */ value: function _createSeperator() { var index = arguments[0] === undefined ? 1 : arguments[0]; this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); } }, { key: '_createAddNodeButton', // ---------------------- DOM functions for buttons --------------------------// value: function _createAddNodeButton(locale) { var button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode'], 'fa-plus'); this.manipulationDiv.appendChild(button); this._bindHammerToDiv(button, this.addNodeMode.bind(this)); } }, { key: '_createAddEdgeButton', value: function _createAddEdgeButton(locale) { var button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge'] || this.options.locales['en']['addEdge'], 'fa-plus'); this.manipulationDiv.appendChild(button); this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); } }, { key: '_createEditNodeButton', value: function _createEditNodeButton(locale) { var button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode'], 'fa-pencil'); this.manipulationDiv.appendChild(button); this._bindHammerToDiv(button, this.editNode.bind(this)); } }, { key: '_createEditEdgeButton', value: function _createEditEdgeButton(locale) { var button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge'] || this.options.locales['en']['editEdge'], 'fa-pencil'); this.manipulationDiv.appendChild(button); this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); } }, { key: '_createDeleteButton', value: function _createDeleteButton(locale) { var button = this._createButton('delete', 'vis-button vis-delete', locale['del'] || this.options.locales['en']['del'], 'fa-minus'); this.manipulationDiv.appendChild(button); this._bindHammerToDiv(button, this.deleteSelected.bind(this)); } }, { key: '_createBackButton', value: function _createBackButton(locale) { var button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back'], 'fa-arrow-left'); this.manipulationDiv.appendChild(button); this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); } }, { key: '_createButton', value: function _createButton(id, className, label, iconClass) { var labelClassName = arguments[4] === undefined ? 'vis-label' : arguments[4]; this.manipulationDOM[id + 'Div'] = document.createElement('div'); this.manipulationDOM[id + 'Div'].className = className; this.manipulationDOM[id + 'Label'] = document.createElement('div'); this.manipulationDOM[id + 'Label'].className = labelClassName; // this.manipulationDOM[id + 'Label'].innerHTML = label; var labelElement = document.createElement('span') if (typeof iconClass != 'undefined' && iconClass != ''){ var iconElement = this._createIcon(iconClass); this.manipulationDOM[id + 'Label'].appendChild(iconElement); labelElement.style.fontFamily = 'inherit'; labelElement.style.fontSize = '0.9em'; labelElement.style.marginLeft = '5px'; } labelElement.innerHTML = label; this.manipulationDOM[id + 'Label'].appendChild(labelElement); this.manipulationDOM[id + 'Div'].appendChild(this.manipulationDOM[id + 'Label']); return this.manipulationDOM[id + 'Div']; } }, { key: '_createDescription', value: function _createDescription(label) { this.manipulationDiv.appendChild(this._createButton('description', 'vis-button vis-none', label, '')); } }, { key: '_createIcon', value: function _createIcon(iconClass){ var newIcon = document.createElement('i'); newIcon.classList.add('fa'); newIcon.classList.add(iconClass); return newIcon } }, { key: '_temporaryBindEvent', // -------------------------- End of DOM functions for buttons ------------------------------// /** * this binds an event until cleanup by the clean functions. * @param event * @param newFunction * @private */ value: function _temporaryBindEvent(event, newFunction) { this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); this.body.emitter.on(event, newFunction); } }, { key: '_temporaryBindUI', /** * this overrides an UI function until cleanup by the clean function * @param UIfunctionName * @param newFunction * @private */ value: function _temporaryBindUI(UIfunctionName, newFunction) { if (this.body.eventListeners[UIfunctionName] !== undefined) { this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; this.body.eventListeners[UIfunctionName] = newFunction; } else { throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); } } }, { key: '_unbindTemporaryUIs', /** * Restore the overridden UI functions to their original state. * * @private */ value: function _unbindTemporaryUIs() { for (var functionName in this.temporaryUIFunctions) { if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; delete this.temporaryUIFunctions[functionName]; } } this.temporaryUIFunctions = {}; } }, { key: '_unbindTemporaryEvents', /** * Unbind the events created by _temporaryBindEvent * @private */ value: function _unbindTemporaryEvents() { for (var i = 0; i < this.temporaryEventFunctions.length; i++) { var eventName = this.temporaryEventFunctions[i].event; var boundFunction = this.temporaryEventFunctions[i].boundFunction; this.body.emitter.off(eventName, boundFunction); } this.temporaryEventFunctions = []; } }, { key: '_bindHammerToDiv', /** * Bind an hammer instance to a DOM element. * @param domElement * @param funct */ value: function _bindHammerToDiv(domElement, boundFunction) { var hammer = new Hammer(domElement, {}); hammerUtil.onTouch(hammer, boundFunction); this.manipulationHammers.push(hammer); } }, { key: '_cleanupTemporaryNodesAndEdges', /** * Neatly clean up temporary edges and nodes * @private */ value: function _cleanupTemporaryNodesAndEdges() { // _clean temporary edges for (var i = 0; i < this.temporaryIds.edges.length; i++) { this.body.edges[this.temporaryIds.edges[i]].disconnect(); delete this.body.edges[this.temporaryIds.edges[i]]; var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); if (indexTempEdge !== -1) { this.body.edgeIndices.splice(indexTempEdge, 1); } } // _clean temporary nodes for (var i = 0; i < this.temporaryIds.nodes.length; i++) { delete this.body.nodes[this.temporaryIds.nodes[i]]; var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); if (indexTempNode !== -1) { this.body.nodeIndices.splice(indexTempNode, 1); } } this.temporaryIds = { nodes: [], edges: [] }; } }, { key: '_controlNodeTouch', // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// /** * the touch is used to get the position of the initial click * @param event * @private */ value: function _controlNodeTouch(event) { this.selectionHandler.unselectAll(); this.lastTouch = this.body.functions.getPointer(event.center); this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object } }, { key: '_controlNodeDragStart', /** * the drag start is used to mark one of the control nodes as selected. * @param event * @private */ value: function _controlNodeDragStart(event) { var pointer = this.lastTouch; var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); var from = this.body.nodes[this.temporaryIds.nodes[0]]; var to = this.body.nodes[this.temporaryIds.nodes[1]]; var edge = this.body.edges[this.edgeBeingEditedId]; this.selectedControlNode = undefined; var fromSelect = from.isOverlappingWith(pointerObj); var toSelect = to.isOverlappingWith(pointerObj); if (fromSelect === true) { this.selectedControlNode = from; edge.edgeType.from = from; } else if (toSelect === true) { this.selectedControlNode = to; edge.edgeType.to = to; } this.body.emitter.emit('_redraw'); } }, { key: '_controlNodeDrag', /** * dragging the control nodes or the canvas * @param event * @private */ value: function _controlNodeDrag(event) { this.body.emitter.emit('disablePhysics'); var pointer = this.body.functions.getPointer(event.center); var pos = this.canvas.DOMtoCanvas(pointer); if (this.selectedControlNode !== undefined) { this.selectedControlNode.x = pos.x; this.selectedControlNode.y = pos.y; } else { // if the drag was not started properly because the click started outside the network div, start it now. var diffX = pointer.x - this.lastTouch.x; var diffY = pointer.y - this.lastTouch.y; this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; } this.body.emitter.emit('_redraw'); } }, { key: '_controlNodeDragEnd', /** * connecting or restoring the control nodes. * @param event * @private */ value: function _controlNodeDragEnd(event) { var pointer = this.body.functions.getPointer(event.center); var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); var edge = this.body.edges[this.edgeBeingEditedId]; var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); var node = undefined; for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { if (overlappingNodeIds[i] !== this.selectedControlNode.id) { node = this.body.nodes[overlappingNodeIds[i]]; break; } } // perform the connection if (node !== undefined && this.selectedControlNode !== undefined) { if (node.isCluster === true) { alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); } else { var from = this.body.nodes[this.temporaryIds.nodes[0]]; if (this.selectedControlNode.id === from.id) { this._performEditEdge(node.id, edge.to.id); } else { this._performEditEdge(edge.from.id, node.id); } } } else { edge.updateEdgeType(); this.body.emitter.emit('restorePhysics'); } this.body.emitter.emit('_redraw'); } }, { key: '_handleConnect', // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** * the function bound to the selection event. It checks if you want to connect a cluster and changes the description * to walk the user through the process. * * @private */ value: function _handleConnect(event) { // check to avoid double fireing of this function. if (new Date().valueOf() - this.touchTime > 100) { this.lastTouch = this.body.functions.getPointer(event.center); this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object var pointer = this.lastTouch; var node = this.selectionHandler.getNodeAt(pointer); if (node !== undefined) { if (node.isCluster === true) { alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); } else { // create a node the temporary line can look at var targetNode = this._getNewTargetNode(node.x, node.y); this.body.nodes[targetNode.id] = targetNode; this.body.nodeIndices.push(targetNode.id); // create a temporary edge var connectionEdge = this.body.functions.createEdge({ id: 'connectionEdge' + util.randomUUID(), from: node.id, to: targetNode.id, physics: false, smooth: { enabled: true, type: 'continuous', roundness: 0.5 } }); this.body.edges[connectionEdge.id] = connectionEdge; this.body.edgeIndices.push(connectionEdge.id); this.temporaryIds.nodes.push(targetNode.id); this.temporaryIds.edges.push(connectionEdge.id); } } this.touchTime = new Date().valueOf(); } } }, { key: '_dragControlNode', value: function _dragControlNode(event) { var pointer = this.body.functions.getPointer(event.center); if (this.temporaryIds.nodes[0] !== undefined) { var targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode. targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x); targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y); this.body.emitter.emit('_redraw'); } else { var diffX = pointer.x - this.lastTouch.x; var diffY = pointer.y - this.lastTouch.y; this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; } } }, { key: '_finishConnect', /** * Connect the new edge to the target if one exists, otherwise remove temp line * @param event * @private */ value: function _finishConnect(event) { var pointer = this.body.functions.getPointer(event.center); var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); // remember the edge id var connectFromId = undefined; if (this.temporaryIds.edges[0] !== undefined) { connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; } // get the overlapping node but NOT the temporary node; var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); var node = undefined; for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { // if the node id is NOT a temporary node, accept the node. if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) { node = this.body.nodes[overlappingNodeIds[i]]; break; } } // clean temporary nodes and edges. this._cleanupTemporaryNodesAndEdges(); // perform the connection if (node !== undefined) { if (node.isCluster === true) { alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); } else { if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { this._performAddEdge(connectFromId, node.id); } } } this.body.emitter.emit('_redraw'); } }, { key: '_performAddNode', // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// // ------------------------------ Performing all the actual data manipulation ------------------------// /** * Adds a node on the specified location */ value: function _performAddNode(clickData) { var _this5 = this; var defaultData = { id: util.randomUUID(), x: clickData.pointer.canvas.x, y: clickData.pointer.canvas.y, label: 'new' }; if (typeof this.options.addNode === 'function') { if (this.options.addNode.length === 2) { this.options.addNode(defaultData, function (finalizedData) { if (finalizedData !== null && finalizedData !== undefined && _this5.inMode === 'addNode') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback _this5.body.data.nodes.getDataSet().add(finalizedData); _this5.showManipulatorToolbar(); } }); } else { throw new Error('The function for add does not support two arguments (data,callback)'); this.showManipulatorToolbar(); } } else { this.body.data.nodes.getDataSet().add(defaultData); this.showManipulatorToolbar(); } } }, { key: '_performAddEdge', /** * connect two nodes with a new edge. * * @private */ value: function _performAddEdge(sourceNodeId, targetNodeId) { var _this6 = this; var defaultData = { from: sourceNodeId, to: targetNodeId }; if (typeof this.options.addEdge === 'function') { if (this.options.addEdge.length === 2) { this.options.addEdge(defaultData, function (finalizedData) { if (finalizedData !== null && finalizedData !== undefined && _this6.inMode === 'addEdge') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback _this6.body.data.edges.add(finalizedData); _this6.selectionHandler.unselectAll(); _this6.showManipulatorToolbar(); } }); } else { throw new Error('The function for connect does not support two arguments (data,callback)'); } } else { this.body.data.edges.add(defaultData); this.selectionHandler.unselectAll(); this.showManipulatorToolbar(); } } }, { key: '_performEditEdge', /** * connect two nodes with a new edge. * * @private */ value: function _performEditEdge(sourceNodeId, targetNodeId) { var _this7 = this; var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; if (typeof this.options.editEdge === 'function') { if (this.options.editEdge.length === 2) { this.options.editEdge(defaultData, function (finalizedData) { if (finalizedData === null || finalizedData === undefined || _this7.inMode !== 'editEdge') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { _this7.body.edges[defaultData.id].updateEdgeType(); _this7.body.emitter.emit('_redraw'); } else { _this7.body.data.edges.update(finalizedData); _this7.selectionHandler.unselectAll(); _this7.showManipulatorToolbar(); } }); } else { throw new Error('The function for edit does not support two arguments (data, callback)'); } } else { this.body.data.edges.update(defaultData); this.selectionHandler.unselectAll(); this.showManipulatorToolbar(); } } }]); return ManipulationSystem; })(); exports['default'] = ManipulationSystem; module.exports = exports['default']; /***/ }, /* 108 */ /***/ function(module, exports) { /** * This object contains all possible options. It will check if the types are correct, if required if the option is one * of the allowed values. * * __any__ means that the name of the property does not matter. * __type__ is a required field for all objects and contains the allowed types of all objects */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var string = 'string'; var boolean = 'boolean'; var number = 'number'; var array = 'array'; var object = 'object'; // should only be in a __type__ property var dom = 'dom'; var any = 'any'; var allOptions = { configure: { enabled: { boolean: boolean }, filter: { boolean: boolean, string: string, array: array, 'function': 'function' }, container: { dom: dom }, showButton: { boolean: boolean }, __type__: { object: object, boolean: boolean, string: string, array: array, 'function': 'function' } }, edges: { arrows: { to: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, middle: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, from: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, __type__: { string: ['from', 'to', 'middle'], object: object } }, color: { color: { string: string }, highlight: { string: string }, hover: { string: string }, inherit: { string: ['from', 'to', 'both'], boolean: boolean }, opacity: { number: number }, __type__: { object: object, string: string } }, dashes: { boolean: boolean, array: array }, font: { color: { string: string }, size: { number: number }, // px face: { string: string }, background: { string: string }, strokeWidth: { number: number }, // px strokeColor: { string: string }, align: { string: ['horizontal', 'top', 'middle', 'bottom'] }, __type__: { object: object, string: string } }, hidden: { boolean: boolean }, hoverWidth: { 'function': 'function', number: number }, label: { string: string, 'undefined': 'undefined' }, length: { number: number, 'undefined': 'undefined' }, physics: { boolean: boolean }, scaling: { min: { number: number }, max: { number: number }, label: { enabled: { boolean: boolean }, min: { number: number }, max: { number: number }, maxVisible: { number: number }, drawThreshold: { number: number }, __type__: { object: object, boolean: boolean } }, customScalingFunction: { 'function': 'function' }, __type__: { object: object } }, selectionWidth: { 'function': 'function', number: number }, selfReferenceSize: { number: number }, shadow: { enabled: { boolean: boolean }, size: { number: number }, x: { number: number }, y: { number: number }, __type__: { object: object, boolean: boolean } }, smooth: { enabled: { boolean: boolean }, type: { string: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'] }, roundness: { number: number }, __type__: { object: object, boolean: boolean } }, title: { string: string, 'undefined': 'undefined' }, width: { number: number }, value: { number: number, 'undefined': 'undefined' }, __type__: { object: object } }, groups: { useDefaultGroups: { boolean: boolean }, __any__: 'get from nodes, will be overwritten below', __type__: { object: object } }, interaction: { dragNodes: { boolean: boolean }, dragView: { boolean: boolean }, hideEdgesOnDrag: { boolean: boolean }, hideNodesOnDrag: { boolean: boolean }, hover: { boolean: boolean }, keyboard: { enabled: { boolean: boolean }, speed: { x: { number: number }, y: { number: number }, zoom: { number: number }, __type__: { object: object } }, bindToWindow: { boolean: boolean }, __type__: { object: object, boolean: boolean } }, multiselect: { boolean: boolean }, navigationButtons: { boolean: boolean }, selectable: { boolean: boolean }, selectConnectedEdges: { boolean: boolean }, hoverConnectedEdges: { boolean: boolean }, tooltipDelay: { number: number }, zoomView: { boolean: boolean }, __type__: { object: object } }, layout: { randomSeed: { 'undefined': 'undefined', number: number }, hierarchical: { enabled: { boolean: boolean }, levelSeparation: { number: number }, direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed __type__: { object: object, boolean: boolean } }, __type__: { object: object } }, manipulation: { enabled: { boolean: boolean }, initiallyActive: { boolean: boolean }, addNode: { boolean: boolean, 'function': 'function' }, addEdge: { boolean: boolean, 'function': 'function' }, editNode: { 'function': 'function' }, editEdge: { boolean: boolean, 'function': 'function' }, deleteNode: { boolean: boolean, 'function': 'function' }, deleteEdge: { boolean: boolean, 'function': 'function' }, controlNodeStyle: 'get from nodes, will be overwritten below', __type__: { object: object, boolean: boolean } }, nodes: { borderWidth: { number: number }, borderWidthSelected: { number: number, 'undefined': 'undefined' }, brokenImage: { string: string, 'undefined': 'undefined' }, color: { border: { string: string }, background: { string: string }, highlight: { border: { string: string }, background: { string: string }, __type__: { object: object, string: string } }, hover: { border: { string: string }, background: { string: string }, __type__: { object: object, string: string } }, __type__: { object: object, string: string } }, fixed: { x: { boolean: boolean }, y: { boolean: boolean }, __type__: { object: object, boolean: boolean } }, font: { color: { string: string }, size: { number: number }, // px face: { string: string }, background: { string: string }, strokeWidth: { number: number }, // px strokeColor: { string: string }, __type__: { object: object, string: string } }, group: { string: string, number: number, 'undefined': 'undefined' }, hidden: { boolean: boolean }, icon: { face: { string: string }, code: { string: string }, //'\uf007', size: { number: number }, //50, color: { string: string }, __type__: { object: object } }, id: { string: string, number: number }, image: { string: string, 'undefined': 'undefined' }, // --> URL label: { string: string, 'undefined': 'undefined' }, level: { number: number, 'undefined': 'undefined' }, mass: { number: number }, physics: { boolean: boolean }, scaling: { min: { number: number }, max: { number: number }, label: { enabled: { boolean: boolean }, min: { number: number }, max: { number: number }, maxVisible: { number: number }, drawThreshold: { number: number }, __type__: { object: object, boolean: boolean } }, customScalingFunction: { 'function': 'function' }, __type__: { object: object } }, shadow: { enabled: { boolean: boolean }, size: { number: number }, x: { number: number }, y: { number: number }, __type__: { object: object, boolean: boolean } }, shape: { string: ['ellipse', 'circle', 'database', 'box', 'text', 'image', 'circularImage', 'diamond', 'dot', 'star', 'triangle', 'triangleDown', 'square', 'icon'] }, size: { number: number }, title: { string: string, 'undefined': 'undefined' }, value: { number: number, 'undefined': 'undefined' }, x: { number: number }, y: { number: number }, __type__: { object: object } }, physics: { enabled: { boolean: boolean }, barnesHut: { gravitationalConstant: { number: number }, centralGravity: { number: number }, springLength: { number: number }, springConstant: { number: number }, damping: { number: number }, avoidOverlap: { number: number }, __type__: { object: object } }, forceAtlas2Based: { gravitationalConstant: { number: number }, centralGravity: { number: number }, springLength: { number: number }, springConstant: { number: number }, damping: { number: number }, avoidOverlap: { number: number }, __type__: { object: object } }, repulsion: { centralGravity: { number: number }, springLength: { number: number }, springConstant: { number: number }, nodeDistance: { number: number }, damping: { number: number }, __type__: { object: object } }, hierarchicalRepulsion: { centralGravity: { number: number }, springLength: { number: number }, springConstant: { number: number }, nodeDistance: { number: number }, damping: { number: number }, __type__: { object: object } }, maxVelocity: { number: number }, minVelocity: { number: number }, // px/s solver: { string: ['barnesHut', 'repulsion', 'hierarchicalRepulsion', 'forceAtlas2Based'] }, stabilization: { enabled: { boolean: boolean }, iterations: { number: number }, // maximum number of iteration to stabilize updateInterval: { number: number }, onlyDynamicEdges: { boolean: boolean }, fit: { boolean: boolean }, __type__: { object: object, boolean: boolean } }, timestep: { number: number }, __type__: { object: object, boolean: boolean } }, //globals : autoResize: { boolean: boolean }, clickToUse: { boolean: boolean }, locale: { string: string }, locales: { __any__: { any: any }, __type__: { object: object } }, height: { string: string }, width: { string: string }, __type__: { object: object } }; allOptions.groups.__any__ = allOptions.nodes; allOptions.manipulation.controlNodeStyle = allOptions.nodes; var configureOptions = { nodes: { borderWidth: [1, 0, 10, 1], borderWidthSelected: [2, 0, 10, 1], color: { border: ['color', '#2B7CE9'], background: ['color', '#97C2FC'], highlight: { border: ['color', '#2B7CE9'], background: ['color', '#D2E5FF'] }, hover: { border: ['color', '#2B7CE9'], background: ['color', '#D2E5FF'] } }, fixed: { x: false, y: false }, font: { color: ['color', '#343434'], size: [14, 0, 100, 1], // px face: ['arial', 'verdana', 'tahoma'], background: ['color', 'none'], strokeWidth: [0, 0, 50, 1], // px strokeColor: ['color', '#ffffff'] }, //group: 'string', hidden: false, //icon: { // face: 'string', //'FontAwesome', // code: 'string', //'\uf007', // size: [50, 0, 200, 1], //50, // color: ['color','#2B7CE9'] //'#aa00ff' //}, //image: 'string', // --> URL physics: true, scaling: { min: [10, 0, 200, 1], max: [30, 0, 200, 1], label: { enabled: false, min: [14, 0, 200, 1], max: [30, 0, 200, 1], maxVisible: [30, 0, 200, 1], drawThreshold: [5, 0, 20, 1] } }, shadow: { enabled: false, size: [10, 0, 20, 1], x: [5, -30, 30, 1], y: [5, -30, 30, 1] }, shape: ['ellipse', 'box', 'circle', 'database', 'diamond', 'dot', 'square', 'star', 'text', 'triangle', 'triangleDown'], size: [25, 0, 200, 1] }, edges: { arrows: { to: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] } }, color: { color: ['color', '#848484'], highlight: ['color', '#848484'], hover: ['color', '#848484'], inherit: ['from', 'to', 'both', true, false], opacity: [1, 0, 1, 0.05] }, dashes: false, font: { color: ['color', '#343434'], size: [14, 0, 100, 1], // px face: ['arial', 'verdana', 'tahoma'], background: ['color', 'none'], strokeWidth: [2, 0, 50, 1], // px strokeColor: ['color', '#ffffff'], align: ['horizontal', 'top', 'middle', 'bottom'] }, hidden: false, hoverWidth: [1.5, 0, 5, 0.1], physics: true, scaling: { min: [1, 0, 100, 1], max: [15, 0, 100, 1], label: { enabled: true, min: [14, 0, 200, 1], max: [30, 0, 200, 1], maxVisible: [30, 0, 200, 1], drawThreshold: [5, 0, 20, 1] } }, selectionWidth: [1.5, 0, 5, 0.1], selfReferenceSize: [20, 0, 200, 1], shadow: { enabled: false, size: [10, 0, 20, 1], x: [5, -30, 30, 1], y: [5, -30, 30, 1] }, smooth: { enabled: true, type: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'], roundness: [0.5, 0, 1, 0.05] }, width: [1, 0, 30, 1] }, layout: { //randomSeed: [0, 0, 500, 1], hierarchical: { enabled: false, levelSeparation: [150, 20, 500, 5], direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL sortMethod: ['hubsize', 'directed'] // hubsize, directed } }, interaction: { dragNodes: true, dragView: true, hideEdgesOnDrag: false, hideNodesOnDrag: false, hover: false, keyboard: { enabled: false, speed: { x: [10, 0, 40, 1], y: [10, 0, 40, 1], zoom: [0.02, 0, 0.1, 0.005] }, bindToWindow: true }, multiselect: false, navigationButtons: false, selectable: true, selectConnectedEdges: true, hoverConnectedEdges: true, tooltipDelay: [300, 0, 1000, 25], zoomView: true }, manipulation: { enabled: false, initiallyActive: false }, physics: { enabled: true, barnesHut: { //theta: [0.5, 0.1, 1, 0.05], gravitationalConstant: [-2000, -30000, 0, 50], centralGravity: [0.3, 0, 10, 0.05], springLength: [95, 0, 500, 5], springConstant: [0.04, 0, 1.2, 0.005], damping: [0.09, 0, 1, 0.01], avoidOverlap: [0, 0, 1, 0.01] }, forceAtlas2Based: { //theta: [0.5, 0.1, 1, 0.05], gravitationalConstant: [-50, -500, 0, 1], centralGravity: [0.01, 0, 1, 0.005], springLength: [95, 0, 500, 5], springConstant: [0.08, 0, 1.2, 0.005], damping: [0.4, 0, 1, 0.01], avoidOverlap: [0, 0, 1, 0.01] }, repulsion: { centralGravity: [0.2, 0, 10, 0.05], springLength: [200, 0, 500, 5], springConstant: [0.05, 0, 1.2, 0.005], nodeDistance: [100, 0, 500, 5], damping: [0.09, 0, 1, 0.01] }, hierarchicalRepulsion: { centralGravity: [0.2, 0, 10, 0.05], springLength: [100, 0, 500, 5], springConstant: [0.01, 0, 1.2, 0.005], nodeDistance: [120, 0, 500, 5], damping: [0.09, 0, 1, 0.01] }, maxVelocity: [50, 0, 150, 1], minVelocity: [0.1, 0.01, 0.5, 0.01], solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'], timestep: [0.5, 0.01, 1, 0.01] }, global: { locale: ['en', 'nl'] } }; exports.allOptions = allOptions; exports.configureOptions = configureOptions; /***/ }, /* 109 */ /***/ function(module, exports) { /** * Canvas shapes used by Network */ 'use strict'; if (typeof CanvasRenderingContext2D !== 'undefined') { /** * Draw a circle shape */ CanvasRenderingContext2D.prototype.circle = function (x, y, r) { this.beginPath(); this.arc(x, y, r, 0, 2 * Math.PI, false); this.closePath(); }; /** * Draw a square shape * @param {Number} x horizontal center * @param {Number} y vertical center * @param {Number} r size, width and height of the square */ CanvasRenderingContext2D.prototype.square = function (x, y, r) { this.beginPath(); this.rect(x - r, y - r, r * 2, r * 2); this.closePath(); }; /** * Draw a triangle shape * @param {Number} x horizontal center * @param {Number} y vertical center * @param {Number} r radius, half the length of the sides of the triangle */ CanvasRenderingContext2D.prototype.triangle = function (x, y, r) { // http://en.wikipedia.org/wiki/Equilateral_triangle this.beginPath(); // the change in radius and the offset is here to center the shape r *= 1.15; y += 0.275 * r; var s = r * 2; var s2 = s / 2; var ir = Math.sqrt(3) / 6 * s; // radius of inner circle var h = Math.sqrt(s * s - s2 * s2); // height this.moveTo(x, y - (h - ir)); this.lineTo(x + s2, y + ir); this.lineTo(x - s2, y + ir); this.lineTo(x, y - (h - ir)); this.closePath(); }; /** * Draw a triangle shape in downward orientation * @param {Number} x horizontal center * @param {Number} y vertical center * @param {Number} r radius */ CanvasRenderingContext2D.prototype.triangleDown = function (x, y, r) { // http://en.wikipedia.org/wiki/Equilateral_triangle this.beginPath(); // the change in radius and the offset is here to center the shape r *= 1.15; y -= 0.275 * r; var s = r * 2; var s2 = s / 2; var ir = Math.sqrt(3) / 6 * s; // radius of inner circle var h = Math.sqrt(s * s - s2 * s2); // height this.moveTo(x, y + (h - ir)); this.lineTo(x + s2, y - ir); this.lineTo(x - s2, y - ir); this.lineTo(x, y + (h - ir)); this.closePath(); }; /** * Draw a star shape, a star with 5 points * @param {Number} x horizontal center * @param {Number} y vertical center * @param {Number} r radius, half the length of the sides of the triangle */ CanvasRenderingContext2D.prototype.star = function (x, y, r) { // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ this.beginPath(); // the change in radius and the offset is here to center the shape r *= 0.82; y += 0.1 * r; for (var n = 0; n < 10; n++) { var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); } this.closePath(); }; /** * Draw a Diamond shape * @param {Number} x horizontal center * @param {Number} y vertical center * @param {Number} r radius, half the length of the sides of the triangle */ CanvasRenderingContext2D.prototype.diamond = function (x, y, r) { // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ this.beginPath(); this.lineTo(x, y + r); this.lineTo(x + r, y); this.lineTo(x, y - r); this.lineTo(x - r, y); this.closePath(); }; /** * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas */ CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { var r2d = Math.PI / 180; if (w - 2 * r < 0) { r = w / 2; } //ensure that the radius isn't too large for x if (h - 2 * r < 0) { r = h / 2; } //ensure that the radius isn't too large for y this.beginPath(); this.moveTo(x + r, y); this.lineTo(x + w - r, y); this.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); this.lineTo(x + w, y + h - r); this.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); this.lineTo(x + r, y + h); this.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); this.lineTo(x, y + r); this.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); this.closePath(); }; /** * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas */ CanvasRenderingContext2D.prototype.ellipse = function (x, y, w, h) { var kappa = 0.5522848, ox = w / 2 * kappa, // control point offset horizontal oy = h / 2 * kappa, // control point offset vertical xe = x + w, // x-end ye = y + h, // y-end xm = x + w / 2, // x-middle ym = y + h / 2; // y-middle this.beginPath(); this.moveTo(x, ym); this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); this.closePath(); }; /** * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas */ CanvasRenderingContext2D.prototype.database = function (x, y, w, h) { var f = 1 / 3; var wEllipse = w; var hEllipse = h * f; var kappa = 0.5522848, ox = wEllipse / 2 * kappa, // control point offset horizontal oy = hEllipse / 2 * kappa, // control point offset vertical xe = x + wEllipse, // x-end ye = y + hEllipse, // y-end xm = x + wEllipse / 2, // x-middle ym = y + hEllipse / 2, // y-middle ymb = y + (h - hEllipse / 2), // y-midlle, bottom ellipse yeb = y + h; // y-end, bottom ellipse this.beginPath(); this.moveTo(xe, ym); this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); this.lineTo(xe, ymb); this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); this.lineTo(x, ym); }; /** * Draw an arrow point (no line) */ CanvasRenderingContext2D.prototype.arrow = function (x, y, angle, length) { // tail var xt = x - length * Math.cos(angle); var yt = y - length * Math.sin(angle); // inner tail var xi = x - length * 0.9 * Math.cos(angle); var yi = y - length * 0.9 * Math.sin(angle); // left var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); // right var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); this.beginPath(); this.moveTo(x, y); this.lineTo(xl, yl); this.lineTo(xi, yi); this.lineTo(xr, yr); this.closePath(); }; /** * Sets up the dashedLine functionality for drawing * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas * @author David Jordan * @date 2012-08-08 */ CanvasRenderingContext2D.prototype.dashedLine = function (x, y, x2, y2, pattern) { this.beginPath(); this.moveTo(x, y); var patternLength = pattern.length; var dx = x2 - x; var dy = y2 - y; var slope = dy / dx; var distRemaining = Math.sqrt(dx * dx + dy * dy); var patternIndex = 0; var draw = true; var xStep = 0; var dashLength = pattern[0]; while (distRemaining >= 0.1) { dashLength = pattern[patternIndex++ % patternLength]; if (dashLength > distRemaining) { dashLength = distRemaining; } xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope)); xStep = dx < 0 ? -xStep : xStep; x += xStep; y += slope * xStep; if (draw === true) { this.lineTo(x, y); } else { this.moveTo(x, y); } distRemaining -= dashLength; draw = !draw; } }; } /***/ }, /* 110 */ /***/ function(module, exports) { /** * Parse a text source containing data in DOT language into a JSON object. * The object contains two lists: one with nodes and one with edges. * * DOT language reference: http://www.graphviz.org/doc/info/lang.html * * DOT language attributes: http://graphviz.org/content/attrs * * @param {String} data Text containing a graph in DOT-notation * @return {Object} graph An object containing two parameters: * {Object[]} nodes * {Object[]} edges */ 'use strict'; function parseDOT(data) { dot = data; return parseGraph(); } // mapping of attributes from DOT (the keys) to vis.js (the values) var NODE_ATTR_MAPPING = { 'fontsize': 'font.size', 'fontcolor': 'font.color', 'labelfontcolor': 'font.color', 'fontname': 'font.face', 'color': ['color.border', 'color.background'], 'fillcolor': 'color.background', 'tooltip': 'title', 'labeltooltip': 'title' }; var EDGE_ATTR_MAPPING = Object.create(NODE_ATTR_MAPPING); EDGE_ATTR_MAPPING.color = 'color.color'; // token types enumeration var TOKENTYPE = { NULL: 0, DELIMITER: 1, IDENTIFIER: 2, UNKNOWN: 3 }; // map with all delimiters var DELIMITERS = { '{': true, '}': true, '[': true, ']': true, ';': true, '=': true, ',': true, '->': true, '--': true }; var dot = ''; // current dot file var index = 0; // current index in dot file var c = ''; // current token character in expr var token = ''; // current token var tokenType = TOKENTYPE.NULL; // type of the token /** * Get the first character from the dot file. * The character is stored into the char c. If the end of the dot file is * reached, the function puts an empty string in c. */ function first() { index = 0; c = dot.charAt(0); } /** * Get the next character from the dot file. * The character is stored into the char c. If the end of the dot file is * reached, the function puts an empty string in c. */ function next() { index++; c = dot.charAt(index); } /** * Preview the next character from the dot file. * @return {String} cNext */ function nextPreview() { return dot.charAt(index + 1); } /** * Test whether given character is alphabetic or numeric * @param {String} c * @return {Boolean} isAlphaNumeric */ var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; function isAlphaNumeric(c) { return regexAlphaNumeric.test(c); } /** * Merge all options of object b into object b * @param {Object} a * @param {Object} b * @return {Object} a */ function merge(a, b) { if (!a) { a = {}; } if (b) { for (var name in b) { if (b.hasOwnProperty(name)) { a[name] = b[name]; } } } return a; } /** * Set a value in an object, where the provided parameter name can be a * path with nested parameters. For example: * * var obj = {a: 2}; * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} * * @param {Object} obj * @param {String} path A parameter name or dot-separated parameter path, * like "color.highlight.border". * @param {*} value */ function setValue(obj, path, value) { var keys = path.split('.'); var o = obj; while (keys.length) { var key = keys.shift(); if (keys.length) { // this isn't the end point if (!o[key]) { o[key] = {}; } o = o[key]; } else { // this is the end point o[key] = value; } } } /** * Add a node to a graph object. If there is already a node with * the same id, their attributes will be merged. * @param {Object} graph * @param {Object} node */ function addNode(graph, node) { var i, len; var current = null; // find root graph (in case of subgraph) var graphs = [graph]; // list with all graphs from current graph to root graph var root = graph; while (root.parent) { graphs.push(root.parent); root = root.parent; } // find existing node (at root level) by its id if (root.nodes) { for (i = 0, len = root.nodes.length; i < len; i++) { if (node.id === root.nodes[i].id) { current = root.nodes[i]; break; } } } if (!current) { // this is a new node current = { id: node.id }; if (graph.node) { // clone default attributes current.attr = merge(current.attr, graph.node); } } // add node to this (sub)graph and all its parent graphs for (i = graphs.length - 1; i >= 0; i--) { var g = graphs[i]; if (!g.nodes) { g.nodes = []; } if (g.nodes.indexOf(current) === -1) { g.nodes.push(current); } } // merge attributes if (node.attr) { current.attr = merge(current.attr, node.attr); } } /** * Add an edge to a graph object * @param {Object} graph * @param {Object} edge */ function addEdge(graph, edge) { if (!graph.edges) { graph.edges = []; } graph.edges.push(edge); if (graph.edge) { var attr = merge({}, graph.edge); // clone default attributes edge.attr = merge(attr, edge.attr); // merge attributes } } /** * Create an edge to a graph object * @param {Object} graph * @param {String | Number | Object} from * @param {String | Number | Object} to * @param {String} type * @param {Object | null} attr * @return {Object} edge */ function createEdge(graph, from, to, type, attr) { var edge = { from: from, to: to, type: type }; if (graph.edge) { edge.attr = merge({}, graph.edge); // clone default attributes } edge.attr = merge(edge.attr || {}, attr); // merge attributes return edge; } /** * Get next token in the current dot file. * The token and token type are available as token and tokenType */ function getToken() { tokenType = TOKENTYPE.NULL; token = ''; // skip over whitespaces while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter next(); } do { var isComment = false; // skip comment if (c === '#') { // find the previous non-space character var i = index - 1; while (dot.charAt(i) === ' ' || dot.charAt(i) === '\t') { i--; } if (dot.charAt(i) === '\n' || dot.charAt(i) === '') { // the # is at the start of a line, this is indeed a line comment while (c != '' && c != '\n') { next(); } isComment = true; } } if (c === '/' && nextPreview() === '/') { // skip line comment while (c != '' && c != '\n') { next(); } isComment = true; } if (c === '/' && nextPreview() === '*') { // skip block comment while (c != '') { if (c === '*' && nextPreview() === '/') { // end of block comment found. skip these last two characters next(); next(); break; } else { next(); } } isComment = true; } // skip over whitespaces while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter next(); } } while (isComment); // check for end of dot file if (c === '') { // token is still empty tokenType = TOKENTYPE.DELIMITER; return; } // check for delimiters consisting of 2 characters var c2 = c + nextPreview(); if (DELIMITERS[c2]) { tokenType = TOKENTYPE.DELIMITER; token = c2; next(); next(); return; } // check for delimiters consisting of 1 character if (DELIMITERS[c]) { tokenType = TOKENTYPE.DELIMITER; token = c; next(); return; } // check for an identifier (number or string) // TODO: more precise parsing of numbers/strings (and the port separator ':') if (isAlphaNumeric(c) || c === '-') { token += c; next(); while (isAlphaNumeric(c)) { token += c; next(); } if (token === 'false') { token = false; // convert to boolean } else if (token === 'true') { token = true; // convert to boolean } else if (!isNaN(Number(token))) { token = Number(token); // convert to number } tokenType = TOKENTYPE.IDENTIFIER; return; } // check for a string enclosed by double quotes if (c === '"') { next(); while (c != '' && (c != '"' || c === '"' && nextPreview() === '"')) { token += c; if (c === '"') { // skip the escape character next(); } next(); } if (c != '"') { throw newSyntaxError('End of string " expected'); } next(); tokenType = TOKENTYPE.IDENTIFIER; return; } // something unknown is found, wrong characters, a syntax error tokenType = TOKENTYPE.UNKNOWN; while (c != '') { token += c; next(); } throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); } /** * Parse a graph. * @returns {Object} graph */ function parseGraph() { var graph = {}; first(); getToken(); // optional strict keyword if (token === 'strict') { graph.strict = true; getToken(); } // graph or digraph keyword if (token === 'graph' || token === 'digraph') { graph.type = token; getToken(); } // optional graph id if (tokenType === TOKENTYPE.IDENTIFIER) { graph.id = token; getToken(); } // open angle bracket if (token != '{') { throw newSyntaxError('Angle bracket { expected'); } getToken(); // statements parseStatements(graph); // close angle bracket if (token != '}') { throw newSyntaxError('Angle bracket } expected'); } getToken(); // end of file if (token !== '') { throw newSyntaxError('End of file expected'); } getToken(); // remove temporary default options delete graph.node; delete graph.edge; delete graph.graph; return graph; } /** * Parse a list with statements. * @param {Object} graph */ function parseStatements(graph) { while (token !== '' && token != '}') { parseStatement(graph); if (token === ';') { getToken(); } } } /** * Parse a single statement. Can be a an attribute statement, node * statement, a series of node statements and edge statements, or a * parameter. * @param {Object} graph */ function parseStatement(graph) { // parse subgraph var subgraph = parseSubgraph(graph); if (subgraph) { // edge statements parseEdge(graph, subgraph); return; } // parse an attribute statement var attr = parseAttributeStatement(graph); if (attr) { return; } // parse node if (tokenType != TOKENTYPE.IDENTIFIER) { throw newSyntaxError('Identifier expected'); } var id = token; // id can be a string or a number getToken(); if (token === '=') { // id statement getToken(); if (tokenType != TOKENTYPE.IDENTIFIER) { throw newSyntaxError('Identifier expected'); } graph[id] = token; getToken(); // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " } else { parseNodeStatement(graph, id); } } /** * Parse a subgraph * @param {Object} graph parent graph object * @return {Object | null} subgraph */ function parseSubgraph(graph) { var subgraph = null; // optional subgraph keyword if (token === 'subgraph') { subgraph = {}; subgraph.type = 'subgraph'; getToken(); // optional graph id if (tokenType === TOKENTYPE.IDENTIFIER) { subgraph.id = token; getToken(); } } // open angle bracket if (token === '{') { getToken(); if (!subgraph) { subgraph = {}; } subgraph.parent = graph; subgraph.node = graph.node; subgraph.edge = graph.edge; subgraph.graph = graph.graph; // statements parseStatements(subgraph); // close angle bracket if (token != '}') { throw newSyntaxError('Angle bracket } expected'); } getToken(); // remove temporary default options delete subgraph.node; delete subgraph.edge; delete subgraph.graph; delete subgraph.parent; // register at the parent graph if (!graph.subgraphs) { graph.subgraphs = []; } graph.subgraphs.push(subgraph); } return subgraph; } /** * parse an attribute statement like "node [shape=circle fontSize=16]". * Available keywords are 'node', 'edge', 'graph'. * The previous list with default attributes will be replaced * @param {Object} graph * @returns {String | null} keyword Returns the name of the parsed attribute * (node, edge, graph), or null if nothing * is parsed. */ function parseAttributeStatement(graph) { // attribute statements if (token === 'node') { getToken(); // node attributes graph.node = parseAttributeList(); return 'node'; } else if (token === 'edge') { getToken(); // edge attributes graph.edge = parseAttributeList(); return 'edge'; } else if (token === 'graph') { getToken(); // graph attributes graph.graph = parseAttributeList(); return 'graph'; } return null; } /** * parse a node statement * @param {Object} graph * @param {String | Number} id */ function parseNodeStatement(graph, id) { // node statement var node = { id: id }; var attr = parseAttributeList(); if (attr) { node.attr = attr; } addNode(graph, node); // edge statements parseEdge(graph, id); } /** * Parse an edge or a series of edges * @param {Object} graph * @param {String | Number} from Id of the from node */ function parseEdge(graph, from) { while (token === '->' || token === '--') { var to; var type = token; getToken(); var subgraph = parseSubgraph(graph); if (subgraph) { to = subgraph; } else { if (tokenType != TOKENTYPE.IDENTIFIER) { throw newSyntaxError('Identifier or subgraph expected'); } to = token; addNode(graph, { id: to }); getToken(); } // parse edge attributes var attr = parseAttributeList(); // create edge var edge = createEdge(graph, from, to, type, attr); addEdge(graph, edge); from = to; } } /** * Parse a set with attributes, * for example [label="1.000", shape=solid] * @return {Object | null} attr */ function parseAttributeList() { var attr = null; while (token === '[') { getToken(); attr = {}; while (token !== '' && token != ']') { if (tokenType != TOKENTYPE.IDENTIFIER) { throw newSyntaxError('Attribute name expected'); } var name = token; getToken(); if (token != '=') { throw newSyntaxError('Equal sign = expected'); } getToken(); if (tokenType != TOKENTYPE.IDENTIFIER) { throw newSyntaxError('Attribute value expected'); } var value = token; setValue(attr, name, value); // name can be a path getToken(); if (token == ',') { getToken(); } } if (token != ']') { throw newSyntaxError('Bracket ] expected'); } getToken(); } return attr; } /** * Create a syntax error with extra information on current token and index. * @param {String} message * @returns {SyntaxError} err */ function newSyntaxError(message) { return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); } /** * Chop off text after a maximum length * @param {String} text * @param {Number} maxLength * @returns {String} */ function chop(text, maxLength) { return text.length <= maxLength ? text : text.substr(0, 27) + '...'; } /** * Execute a function fn for each pair of elements in two arrays * @param {Array | *} array1 * @param {Array | *} array2 * @param {function} fn */ function forEach2(array1, array2, fn) { if (Array.isArray(array1)) { array1.forEach(function (elem1) { if (Array.isArray(array2)) { array2.forEach(function (elem2) { fn(elem1, elem2); }); } else { fn(elem1, array2); } }); } else { if (Array.isArray(array2)) { array2.forEach(function (elem2) { fn(array1, elem2); }); } else { fn(array1, array2); } } } /** * Set a nested property on an object * When nested objects are missing, they will be created. * For example setProp({}, 'font.color', 'red') will return {font: {color: 'red'}} * @param {Object} object * @param {string} path A dot separated string like 'font.color' * @param {*} value Value for the property * @return {Object} Returns the original object, allows for chaining. */ function setProp(object, path, value) { var names = path.split('.'); var prop = names.pop(); // traverse over the nested objects var obj = object; for (var i = 0; i < names.length; i++) { var name = names[i]; if (!(name in obj)) { obj[name] = {}; } obj = obj[name]; } // set the property value obj[prop] = value; return object; } /** * Convert an object with DOT attributes to their vis.js equivalents. * @param {Object} attr Object with DOT attributes * @param {Object} mapping * @return {Object} Returns an object with vis.js attributes */ function convertAttr(attr, mapping) { var converted = {}; for (var prop in attr) { if (attr.hasOwnProperty(prop)) { var visProp = mapping[prop]; if (Array.isArray(visProp)) { visProp.forEach(function (visPropI) { setProp(converted, visPropI, attr[prop]); }); } else if (typeof visProp === 'string') { setProp(converted, visProp, attr[prop]); } else { setProp(converted, prop, attr[prop]); } } } return converted; } /** * Convert a string containing a graph in DOT language into a map containing * with nodes and edges in the format of graph. * @param {String} data Text containing a graph in DOT-notation * @return {Object} graphData */ function DOTToGraph(data) { // parse the DOT file var dotData = parseDOT(data); var graphData = { nodes: [], edges: [], options: {} }; // copy the nodes if (dotData.nodes) { dotData.nodes.forEach(function (dotNode) { var graphNode = { id: dotNode.id, label: String(dotNode.label || dotNode.id) }; merge(graphNode, convertAttr(dotNode.attr, NODE_ATTR_MAPPING)); if (graphNode.image) { graphNode.shape = 'image'; } graphData.nodes.push(graphNode); }); } // copy the edges if (dotData.edges) { /** * Convert an edge in DOT format to an edge with VisGraph format * @param {Object} dotEdge * @returns {Object} graphEdge */ var convertEdge = function convertEdge(dotEdge) { var graphEdge = { from: dotEdge.from, to: dotEdge.to }; merge(graphEdge, convertAttr(dotEdge.attr, EDGE_ATTR_MAPPING)); graphEdge.arrows = dotEdge.type === '->' ? 'to' : undefined; return graphEdge; }; dotData.edges.forEach(function (dotEdge) { var from, to; if (dotEdge.from instanceof Object) { from = dotEdge.from.nodes; } else { from = { id: dotEdge.from }; } // TODO: support of solid/dotted/dashed edges (attr = 'style') // TODO: support for attributes 'dir' and 'arrowhead' (edge arrows) if (dotEdge.to instanceof Object) { to = dotEdge.to.nodes; } else { to = { id: dotEdge.to }; } if (dotEdge.from instanceof Object && dotEdge.from.edges) { dotEdge.from.edges.forEach(function (subEdge) { var graphEdge = convertEdge(subEdge); graphData.edges.push(graphEdge); }); } forEach2(from, to, function (from, to) { var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); var graphEdge = convertEdge(subEdge); graphData.edges.push(graphEdge); }); if (dotEdge.to instanceof Object && dotEdge.to.edges) { dotEdge.to.edges.forEach(function (subEdge) { var graphEdge = convertEdge(subEdge); graphData.edges.push(graphEdge); }); } }); } // copy the options if (dotData.attr) { graphData.options = dotData.attr; } return graphData; } // exports exports.parseDOT = parseDOT; exports.DOTToGraph = DOTToGraph; /***/ }, /* 111 */ /***/ function(module, exports) { 'use strict'; function parseGephi(gephiJSON, optionsObj) { var edges = []; var nodes = []; var options = { edges: { inheritColor: false }, nodes: { fixed: false, parseColor: false } }; if (optionsObj !== undefined) { if (optionsObj.fixed !== undefined) { options.nodes.fixed = optionsObj.fixed; } if (optionsObj.parseColor !== undefined) { options.nodes.parseColor = optionsObj.parseColor; } if (optionsObj.inheritColor !== undefined) { options.edges.inheritColor = optionsObj.inheritColor; } } var gEdges = gephiJSON.edges; var gNodes = gephiJSON.nodes; for (var i = 0; i < gEdges.length; i++) { var edge = {}; var gEdge = gEdges[i]; edge['id'] = gEdge.id; edge['from'] = gEdge.source; edge['to'] = gEdge.target; edge['attributes'] = gEdge.attributes; // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; if (gEdge.color && options.inheritColor === false) { edge['color'] = gEdge.color; } edges.push(edge); } for (var i = 0; i < gNodes.length; i++) { var node = {}; var gNode = gNodes[i]; node['id'] = gNode.id; node['attributes'] = gNode.attributes; node['title'] = gNode.title; node['x'] = gNode.x; node['y'] = gNode.y; node['label'] = gNode.label; if (options.nodes.parseColor === true) { node['color'] = gNode.color; } else { node['color'] = gNode.color !== undefined ? { background: gNode.color, border: gNode.color, highlight: { background: gNode.color, border: gNode.color }, hover: { background: gNode.color, border: gNode.color } } : undefined; } node['size'] = gNode.size; node['fixed'] = options.nodes.fixed && gNode.x !== undefined && gNode.y !== undefined; nodes.push(node); } return { nodes: nodes, edges: edges }; } exports.parseGephi = parseGephi; /***/ }, /* 112 */ /***/ function(module, exports) { /** * @class Images * This class loads images and keeps them stored. */ "use strict"; function Images(callback) { this.images = {}; this.imageBroken = {}; this.callback = callback; } /** * * @param {string} url Url of the image * @param {string} url Url of an image to use if the url image is not found * @return {Image} img The image object */ Images.prototype.load = function (url, brokenUrl, id) { var img = this.images[url]; // make a pointer if (img === undefined) { // create the image var me = this; img = new Image(); img.onload = function () { // IE11 fix -- thanks dponch! if (this.width === 0) { document.body.appendChild(this); this.width = this.offsetWidth; this.height = this.offsetHeight; document.body.removeChild(this); } if (me.callback) { me.images[url] = img; me.callback(this); } }; img.onerror = function () { if (brokenUrl === undefined) { console.error("Could not load image:", url); delete this.src; if (me.callback) { me.callback(this); } } else { if (me.imageBroken[id] && me.imageBroken[id][url] === true) { console.error("Could not load brokenImage:", brokenUrl); delete this.src; if (me.callback) { me.callback(this); } } else { console.error("Could not load image:", url); this.src = brokenUrl; if (me.imageBroken[id] === undefined) { me.imageBroken[id] = {}; } me.imageBroken[id][url] = true; } } }; img.src = url; } return img; }; module.exports = Images; /***/ }, /* 113 */ /***/ function(module, exports) { // English 'use strict'; exports['en'] = { edit: 'Edit', del: 'Delete selected', back: 'Back', addNode: 'Add Node', addEdge: 'Add Edge', editNode: 'Edit Node', editEdge: 'Edit Edge', addDescription: 'Click in an empty space to place a new node.', edgeDescription: 'Click on a node and drag the edge to another node to connect them.', editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.', createEdgeError: 'Cannot link edges to a cluster.', deleteClusterError: 'Clusters cannot be deleted.', editClusterError: 'Clusters cannot be edited.' }; exports['en_EN'] = exports['en']; exports['en_US'] = exports['en']; // Dutch exports['nl'] = { edit: 'Wijzigen', del: 'Selectie verwijderen', back: 'Terug', addNode: 'Node toevoegen', addEdge: 'Link toevoegen', editNode: 'Node wijzigen', editEdge: 'Link wijzigen', addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.', edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.', editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.', createEdgeError: 'Kan geen link maken naar een cluster.', deleteClusterError: 'Clusters kunnen niet worden verwijderd.', editClusterError: 'Clusters kunnen niet worden aangepast.' }; exports['nl_NL'] = exports['nl']; exports['nl_BE'] = exports['nl']; /***/ } /******/ ]) }); ; ././@LongLink0000000000000000000000000000017300000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/libs/angular-material.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/li0000664000175000017500000457231313737340771034026 0ustar zuulzuul00000000000000/*! * AngularJS Material Design * https://github.com/angular/material * @license MIT * v1.1.5 */ (function( window, angular, undefined ){ "use strict"; (function(){ "use strict"; angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.chips","material.components.checkbox","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.icon","material.components.gridList","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.panel","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.truncate","material.components.virtualRepeat","material.components.whiteframe"]); })(); (function(){ "use strict"; /** * Initialization function that validates environment * requirements. */ DetectNgTouch.$inject = ["$log", "$injector"]; MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; rAFDecorator.$inject = ["$delegate"]; qDecorator.$inject = ["$delegate"]; angular .module('material.core', [ 'ngAnimate', 'material.core.animate', 'material.core.layout', 'material.core.interaction', 'material.core.gestures', 'material.core.theming' ]) .config(MdCoreConfigure) .run(DetectNgTouch); /** * Detect if the ng-Touch module is also being used. * Warn if detected. * @ngInject */ function DetectNgTouch($log, $injector) { if ( $injector.has('$swipe') ) { var msg = "" + "You are using the ngTouch module. \n" + "AngularJS Material already has mobile click, tap, and swipe support... \n" + "ngTouch is not supported with AngularJS Material!"; $log.warn(msg); } } /** * @ngInject */ function MdCoreConfigure($provide, $mdThemingProvider) { $provide.decorator('$$rAF', ['$delegate', rAFDecorator]); $provide.decorator('$q', ['$delegate', qDecorator]); $mdThemingProvider.theme('default') .primaryPalette('indigo') .accentPalette('pink') .warnPalette('deep-orange') .backgroundPalette('grey'); } /** * @ngInject */ function rAFDecorator($delegate) { /** * Use this to throttle events that come in often. * The throttled function will always use the *last* invocation before the * coming frame. * * For example, window resize events that fire many times a second: * If we set to use an raf-throttled callback on window resize, then * our callback will only be fired once per frame, with the last resize * event that happened before that frame. * * @param {function} callback function to debounce */ $delegate.throttle = function(cb) { var queuedArgs, alreadyQueued, queueCb, context; return function debounced() { queuedArgs = arguments; context = this; queueCb = cb; if (!alreadyQueued) { alreadyQueued = true; $delegate(function() { queueCb.apply(context, Array.prototype.slice.call(queuedArgs)); alreadyQueued = false; }); } }; }; return $delegate; } /** * @ngInject */ function qDecorator($delegate) { /** * Adds a shim for $q.resolve for AngularJS version that don't have it, * so we don't have to think about it. * * via https://github.com/angular/angular.js/pull/11987 */ // TODO(crisbeto): this won't be necessary once we drop AngularJS 1.3 if (!$delegate.resolve) { $delegate.resolve = $delegate.when; } return $delegate; } })(); (function(){ "use strict"; MdAutofocusDirective.$inject = ["$parse"];angular.module('material.core') .directive('mdAutofocus', MdAutofocusDirective) // Support the deprecated md-auto-focus and md-sidenav-focus as well .directive('mdAutoFocus', MdAutofocusDirective) .directive('mdSidenavFocus', MdAutofocusDirective); /** * @ngdoc directive * @name mdAutofocus * @module material.core.util * * @description * * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`, * `$mdBottomSheet`, `$mdMenu` or `$mdSidenav` opens or upon page load for input-like elements. * * When one of these opens, it will find the first nested element with the `[md-autofocus]` * attribute directive and optional expression. An expression may be specified as the directive * value to enable conditional activation of the autofocus. * * @usage * * ### Dialog * * *
* * * * *
*
*
* * ### Bottomsheet * * * Comment Actions * * * * * * {{ item.name }} * * * * * * * * ### Autocomplete * * * {{item.display}} * * * * ### Sidenav * *
* * Left Nav! * * * * Center Content * * Open Left Menu * * * * *
* * * * *
*
*
*
**/ function MdAutofocusDirective($parse) { return { restrict: 'A', link: { pre: preLink } }; function preLink(scope, element, attr) { var attrExp = attr.mdAutoFocus || attr.mdAutofocus || attr.mdSidenavFocus; // Initially update the expression by manually parsing the expression as per $watch source. updateExpression($parse(attrExp)(scope)); // Only watch the expression if it is not empty. if (attrExp) { scope.$watch(attrExp, updateExpression); } /** * Updates the autofocus class which is used to determine whether the attribute * expression evaluates to true or false. * @param {string|boolean} value Attribute Value */ function updateExpression(value) { // Rather than passing undefined to the jqLite toggle class function we explicitly set the // value to true. Otherwise the class will be just toggled instead of being forced. if (angular.isUndefined(value)) { value = true; } element.toggleClass('md-autofocus', !!value); } } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.colorUtil * @description * Color Util */ angular .module('material.core') .factory('$mdColorUtil', ColorUtilFactory); function ColorUtilFactory() { /** * Converts hex value to RGBA string * @param color {string} * @returns {string} */ function hexToRgba (color) { var hex = color[ 0 ] === '#' ? color.substr(1) : color, dig = hex.length / 3, red = hex.substr(0, dig), green = hex.substr(dig, dig), blue = hex.substr(dig * 2); if (dig === 1) { red += red; green += green; blue += blue; } return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)'; } /** * Converts rgba value to hex string * @param color {string} * @returns {string} */ function rgbaToHex(color) { color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); var hex = (color && color.length === 4) ? "#" + ("0" + parseInt(color[1],10).toString(16)).slice(-2) + ("0" + parseInt(color[2],10).toString(16)).slice(-2) + ("0" + parseInt(color[3],10).toString(16)).slice(-2) : ''; return hex.toUpperCase(); } /** * Converts an RGB color to RGBA * @param color {string} * @returns {string} */ function rgbToRgba (color) { return color.replace(')', ', 0.1)').replace('(', 'a('); } /** * Converts an RGBA color to RGB * @param color {string} * @returns {string} */ function rgbaToRgb (color) { return color ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') : 'rgb(0,0,0)'; } return { rgbaToHex: rgbaToHex, hexToRgba: hexToRgba, rgbToRgba: rgbToRgba, rgbaToRgb: rgbaToRgb }; } })(); (function(){ "use strict"; angular.module('material.core') .factory('$mdConstant', MdConstantFactory); /** * Factory function that creates the grab-bag $mdConstant service. * @ngInject */ function MdConstantFactory() { var prefixTestEl = document.createElement('div'); var vendorPrefix = getVendorPrefix(prefixTestEl); var isWebkit = /webkit/i.test(vendorPrefix); var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; function vendorProperty(name) { // Add a dash between the prefix and name, to be able to transform the string into camelcase. var prefixedName = vendorPrefix + '-' + name; var ucPrefix = camelCase(prefixedName); var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1); return hasStyleProperty(prefixTestEl, name) ? name : // The current browser supports the un-prefixed property hasStyleProperty(prefixTestEl, ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property. hasStyleProperty(prefixTestEl, lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase. } function hasStyleProperty(testElement, property) { return angular.isDefined(testElement.style[property]); } function camelCase(input) { return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) { return offset ? letter.toUpperCase() : letter; }); } function getVendorPrefix(testElement) { var prop, match; var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/; for (prop in testElement.style) { if (match = vendorRegex.exec(prop)) { return match[0]; } } } var self = { isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); }, isNumPadKey : function(e) { return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); }, isMetaKey: function(e) { return (e.keyCode >= 91 && e.keyCode <= 93); }, isFnLockKey: function(e) { return (e.keyCode >= 112 && e.keyCode <= 145); }, isNavigationKey : function(e) { var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW]; return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1); }, hasModifierKey: function(e) { return e.ctrlKey || e.metaKey || e.altKey; }, /** * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could * compute this value, but Firefox always reports an element to have a size of zero if it * goes over the max, meaning that we'd have to binary search for the value. */ ELEMENT_MAX_PIXELS: 1533917, /** * Priority for a directive that should run before the directives from ngAria. */ BEFORE_NG_ARIA: 210, /** * Common Keyboard actions and their associated keycode. */ KEY_CODE: { COMMA: 188, SEMICOLON : 186, ENTER: 13, ESCAPE: 27, SPACE: 32, PAGE_UP: 33, PAGE_DOWN: 34, END: 35, HOME: 36, LEFT_ARROW : 37, UP_ARROW : 38, RIGHT_ARROW : 39, DOWN_ARROW : 40, TAB : 9, BACKSPACE: 8, DELETE: 46 }, /** * Vendor prefixed CSS properties to be used to support the given functionality in older browsers * as well. */ CSS: { /* Constants */ TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''), ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''), TRANSFORM: vendorProperty('transform'), TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), TRANSITION: vendorProperty('transition'), TRANSITION_DURATION: vendorProperty('transitionDuration'), ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'), ANIMATION_DURATION: vendorProperty('animationDuration'), ANIMATION_NAME: vendorProperty('animationName'), ANIMATION_TIMING: vendorProperty('animationTimingFunction'), ANIMATION_DIRECTION: vendorProperty('animationDirection') }, /** * As defined in core/style/variables.scss * * $layout-breakpoint-xs: 600px !default; * $layout-breakpoint-sm: 960px !default; * $layout-breakpoint-md: 1280px !default; * $layout-breakpoint-lg: 1920px !default; * */ MEDIA: { 'xs' : '(max-width: 599px)' , 'gt-xs' : '(min-width: 600px)' , 'sm' : '(min-width: 600px) and (max-width: 959px)' , 'gt-sm' : '(min-width: 960px)' , 'md' : '(min-width: 960px) and (max-width: 1279px)' , 'gt-md' : '(min-width: 1280px)' , 'lg' : '(min-width: 1280px) and (max-width: 1919px)', 'gt-lg' : '(min-width: 1920px)' , 'xl' : '(min-width: 1920px)' , 'landscape' : '(orientation: landscape)' , 'portrait' : '(orientation: portrait)' , 'print' : 'print' }, MEDIA_PRIORITY: [ 'xl', 'gt-lg', 'lg', 'gt-md', 'md', 'gt-sm', 'sm', 'gt-xs', 'xs', 'landscape', 'portrait', 'print' ] }; return self; } })(); (function(){ "use strict"; angular .module('material.core') .config( ["$provide", function($provide){ $provide.decorator('$mdUtil', ['$delegate', function ($delegate){ /** * Inject the iterator facade to easily support iteration and accessors * @see iterator below */ $delegate.iterator = MdIterator; return $delegate; } ]); }]); /** * iterator is a list facade to easily support iteration and accessors * * @param items Array list which this iterator will enumerate * @param reloop Boolean enables iterator to consider the list as an endless reloop */ function MdIterator(items, reloop) { var trueFn = function() { return true; }; if (items && !angular.isArray(items)) { items = Array.prototype.slice.call(items); } reloop = !!reloop; var _items = items || [ ]; // Published API return { items: getItems, count: count, inRange: inRange, contains: contains, indexOf: indexOf, itemAt: itemAt, findBy: findBy, add: add, remove: remove, first: first, last: last, next: angular.bind(null, findSubsequentItem, false), previous: angular.bind(null, findSubsequentItem, true), hasPrevious: hasPrevious, hasNext: hasNext }; /** * Publish copy of the enumerable set * @returns {Array|*} */ function getItems() { return [].concat(_items); } /** * Determine length of the list * @returns {Array.length|*|number} */ function count() { return _items.length; } /** * Is the index specified valid * @param index * @returns {Array.length|*|number|boolean} */ function inRange(index) { return _items.length && ( index > -1 ) && (index < _items.length ); } /** * Can the iterator proceed to the next item in the list; relative to * the specified item. * * @param item * @returns {Array.length|*|number|boolean} */ function hasNext(item) { return item ? inRange(indexOf(item) + 1) : false; } /** * Can the iterator proceed to the previous item in the list; relative to * the specified item. * * @param item * @returns {Array.length|*|number|boolean} */ function hasPrevious(item) { return item ? inRange(indexOf(item) - 1) : false; } /** * Get item at specified index/position * @param index * @returns {*} */ function itemAt(index) { return inRange(index) ? _items[index] : null; } /** * Find all elements matching the key/value pair * otherwise return null * * @param val * @param key * * @return array */ function findBy(key, val) { return _items.filter(function(item) { return item[key] === val; }); } /** * Add item to list * @param item * @param index * @returns {*} */ function add(item, index) { if ( !item ) return -1; if (!angular.isNumber(index)) { index = _items.length; } _items.splice(index, 0, item); return indexOf(item); } /** * Remove item from list... * @param item */ function remove(item) { if ( contains(item) ){ _items.splice(indexOf(item), 1); } } /** * Get the zero-based index of the target item * @param item * @returns {*} */ function indexOf(item) { return _items.indexOf(item); } /** * Boolean existence check * @param item * @returns {boolean} */ function contains(item) { return item && (indexOf(item) > -1); } /** * Return first item in the list * @returns {*} */ function first() { return _items.length ? _items[0] : null; } /** * Return last item in the list... * @returns {*} */ function last() { return _items.length ? _items[_items.length - 1] : null; } /** * Find the next item. If reloop is true and at the end of the list, it will go back to the * first item. If given, the `validate` callback will be used to determine whether the next item * is valid. If not valid, it will try to find the next item again. * * @param {boolean} backwards Specifies the direction of searching (forwards/backwards) * @param {*} item The item whose subsequent item we are looking for * @param {Function=} validate The `validate` function * @param {integer=} limit The recursion limit * * @returns {*} The subsequent item or null */ function findSubsequentItem(backwards, item, validate, limit) { validate = validate || trueFn; var curIndex = indexOf(item); while (true) { if (!inRange(curIndex)) return null; var nextIndex = curIndex + (backwards ? -1 : 1); var foundItem = null; if (inRange(nextIndex)) { foundItem = _items[nextIndex]; } else if (reloop) { foundItem = backwards ? last() : first(); nextIndex = indexOf(foundItem); } if ((foundItem === null) || (nextIndex === limit)) return null; if (validate(foundItem)) return foundItem; if (angular.isUndefined(limit)) limit = nextIndex; curIndex = nextIndex; } } } })(); (function(){ "use strict"; mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core') .factory('$mdMedia', mdMediaFactory); /** * @ngdoc service * @name $mdMedia * @module material.core * * @description * `$mdMedia` is used to evaluate whether a given media query is true or false given the * current device's screen / window size. The media query will be re-evaluated on resize, allowing * you to register a watch. * * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
BreakpointmediaQuery
xs(max-width: 599px)
gt-xs(min-width: 600px)
sm(min-width: 600px) and (max-width: 959px)
gt-sm(min-width: 960px)
md(min-width: 960px) and (max-width: 1279px)
gt-md(min-width: 1280px)
lg(min-width: 1280px) and (max-width: 1919px)
gt-lg(min-width: 1920px)
xl(min-width: 1920px)
landscapelandscape
portraitportrait
printprint
* * See Material Design's Layout - Adaptive UI for more details. * * * * * * @returns {boolean} a boolean representing whether or not the given media query is true or false. * * @usage * * app.controller('MyController', function($mdMedia, $scope) { * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) { * $scope.bigScreen = big; * }); * * $scope.screenIsSmall = $mdMedia('sm'); * $scope.customQuery = $mdMedia('(min-width: 1234px)'); * $scope.anotherCustom = $mdMedia('max-width: 300px'); * }); * */ /* @ngInject */ function mdMediaFactory($mdConstant, $rootScope, $window) { var queries = {}; var mqls = {}; var results = {}; var normalizeCache = {}; $mdMedia.getResponsiveAttribute = getResponsiveAttribute; $mdMedia.getQuery = getQuery; $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes; return $mdMedia; function $mdMedia(query) { var validated = queries[query]; if (angular.isUndefined(validated)) { validated = queries[query] = validate(query); } var result = results[validated]; if (angular.isUndefined(result)) { result = add(validated); } return result; } function validate(query) { return $mdConstant.MEDIA[query] || ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); } function add(query) { var result = mqls[query]; if ( !result ) { result = mqls[query] = $window.matchMedia(query); } result.addListener(onQueryChange); return (results[result.media] = !!result.matches); } function onQueryChange(query) { $rootScope.$evalAsync(function() { results[query.media] = !!query.matches; }); } function getQuery(name) { return mqls[name]; } function getResponsiveAttribute(attrs, attrName) { for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) { var mediaName = $mdConstant.MEDIA_PRIORITY[i]; if (!mqls[queries[mediaName]].matches) { continue; } var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); if (attrs[normalizedName]) { return attrs[normalizedName]; } } // fallback on unprefixed return attrs[getNormalizedName(attrs, attrName)]; } function watchResponsiveAttributes(attrNames, attrs, watchFn) { var unwatchFns = []; attrNames.forEach(function(attrName) { var normalizedName = getNormalizedName(attrs, attrName); if (angular.isDefined(attrs[normalizedName])) { unwatchFns.push( attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); } for (var mediaName in $mdConstant.MEDIA) { normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); if (angular.isDefined(attrs[normalizedName])) { unwatchFns.push( attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); } } }); return function unwatch() { unwatchFns.forEach(function(fn) { fn(); }); }; } // Improves performance dramatically function getNormalizedName(attrs, attrName) { return normalizeCache[attrName] || (normalizeCache[attrName] = attrs.$normalize(attrName)); } } })(); (function(){ "use strict"; angular .module('material.core') .config( ["$provide", function($provide) { $provide.decorator('$mdUtil', ['$delegate', function ($delegate) { // Inject the prefixer into our original $mdUtil service. $delegate.prefixer = MdPrefixer; return $delegate; }]); }]); function MdPrefixer(initialAttributes, buildSelector) { var PREFIXES = ['data', 'x']; if (initialAttributes) { // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for // the specified attributes. return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes); } return { buildList: _buildList, buildSelector: _buildSelector, hasAttribute: _hasAttribute, removeAttribute: _removeAttribute }; function _buildList(attributes) { attributes = angular.isArray(attributes) ? attributes : [attributes]; attributes.forEach(function(item) { PREFIXES.forEach(function(prefix) { attributes.push(prefix + '-' + item); }); }); return attributes; } function _buildSelector(attributes) { attributes = angular.isArray(attributes) ? attributes : [attributes]; return _buildList(attributes) .map(function(item) { return '[' + item + ']'; }) .join(','); } function _hasAttribute(element, attribute) { element = _getNativeElement(element); if (!element) { return false; } var prefixedAttrs = _buildList(attribute); for (var i = 0; i < prefixedAttrs.length; i++) { if (element.hasAttribute(prefixedAttrs[i])) { return true; } } return false; } function _removeAttribute(element, attribute) { element = _getNativeElement(element); if (!element) { return; } _buildList(attribute).forEach(function(prefixedAttribute) { element.removeAttribute(prefixedAttribute); }); } /** * Transforms a jqLite or DOM element into a HTML element. * This is useful when supporting jqLite elements and DOM elements at * same time. * @param element {JQLite|Element} Element to be parsed * @returns {HTMLElement} Parsed HTMLElement */ function _getNativeElement(element) { element = element[0] || element; if (element.nodeType) { return element; } } } })(); (function(){ "use strict"; /* * This var has to be outside the angular factory, otherwise when * there are multiple material apps on the same page, each app * will create its own instance of this array and the app's IDs * will not be unique. */ UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"]; var nextUniqueId = 0; /** * @ngdoc module * @name material.core.util * @description * Util */ angular .module('material.core') .factory('$mdUtil', UtilFactory); /** * @ngInject */ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) { // Setup some core variables for the processTemplate method var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')); /** * Checks if the target element has the requested style by key * @param {DOMElement|JQLite} target Target element * @param {string} key Style key * @param {string=} expectedVal Optional expected value * @returns {boolean} Whether the target element has the style or not */ var hasComputedStyle = function (target, key, expectedVal) { var hasValue = false; if ( target && target.length ) { var computedStyles = $window.getComputedStyle(target[0]); hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true); } return hasValue; }; function validateCssValue(value) { return !value ? '0' : hasPx(value) || hasPercent(value) ? value : value + 'px'; } function hasPx(value) { return String(value).indexOf('px') > -1; } function hasPercent(value) { return String(value).indexOf('%') > -1; } var $mdUtil = { dom: {}, now: window.performance && window.performance.now ? angular.bind(window.performance, window.performance.now) : Date.now || function() { return new Date().getTime(); }, /** * Cross-version compatibility method to retrieve an option of a ngModel controller, * which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c). * @param {!angular.ngModelCtrl} ngModelCtrl * @param {!string} optionName * @returns {Object|undefined} */ getModelOption: function (ngModelCtrl, optionName) { if (!ngModelCtrl.$options) { return; } var $options = ngModelCtrl.$options; // The newer versions of AngularJS introduced a `getOption function and made the option values no longer // visible on the $options object. return $options.getOption ? $options.getOption(optionName) : $options[optionName] }, /** * Bi-directional accessor/mutator used to easily update an element's * property based on the current 'dir'ectional value. */ bidi : function(element, property, lValue, rValue) { var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); // If accessor if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl'; // If mutator var elem = angular.element(element); if ( ltr && angular.isDefined(lValue)) { elem.css(property, validateCssValue(lValue)); } else if ( !ltr && angular.isDefined(rValue)) { elem.css(property, validateCssValue(rValue) ); } }, bidiProperty: function (element, lProperty, rProperty, value) { var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); var elem = angular.element(element); if ( ltr && angular.isDefined(lProperty)) { elem.css(lProperty, validateCssValue(value)); elem.css(rProperty, ''); } else if ( !ltr && angular.isDefined(rProperty)) { elem.css(rProperty, validateCssValue(value) ); elem.css(lProperty, ''); } }, clientRect: function(element, offsetParent, isOffsetRect) { var node = getNode(element); offsetParent = getNode(offsetParent || node.offsetParent || document.body); var nodeRect = node.getBoundingClientRect(); // The user can ask for an offsetRect: a rect relative to the offsetParent, // or a clientRect: a rect relative to the page var offsetRect = isOffsetRect ? offsetParent.getBoundingClientRect() : {left: 0, top: 0, width: 0, height: 0}; return { left: nodeRect.left - offsetRect.left, top: nodeRect.top - offsetRect.top, width: nodeRect.width, height: nodeRect.height }; }, offsetRect: function(element, offsetParent) { return $mdUtil.clientRect(element, offsetParent, true); }, // Annoying method to copy nodes to an array, thanks to IE nodesToArray: function(nodes) { nodes = nodes || []; var results = []; for (var i = 0; i < nodes.length; ++i) { results.push(nodes.item(i)); } return results; }, /** * Determines the absolute position of the viewport. * Useful when making client rectangles absolute. * @returns {number} */ getViewportTop: function() { return window.scrollY || window.pageYOffset || 0; }, /** * Finds the proper focus target by searching the DOM. * * @param containerEl * @param attributeVal * @returns {*} */ findFocusTarget: function(containerEl, attributeVal) { var AUTO_FOCUS = this.prefixer('md-autofocus', true); var elToFocus; elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); if ( !elToFocus && attributeVal != AUTO_FOCUS) { // Scan for deprecated attribute elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true)); if ( !elToFocus ) { // Scan for fallback to 'universal' API elToFocus = scanForFocusable(containerEl, AUTO_FOCUS); } } return elToFocus; /** * Can target and nested children for specified Selector (attribute) * whose value may be an expression that evaluates to True/False. */ function scanForFocusable(target, selector) { var elFound, items = target[0].querySelectorAll(selector); // Find the last child element with the focus attribute if ( items && items.length ){ items.length && angular.forEach(items, function(it) { it = angular.element(it); // Check the element for the md-autofocus class to ensure any associated expression // evaluated to true. var isFocusable = it.hasClass('md-autofocus'); if (isFocusable) elFound = it; }); } return elFound; } }, /** * Disables scroll around the passed parent element. * @param element Unused * @param {!Element|!angular.JQLite} parent Element to disable scrolling within. * Defaults to body if none supplied. * @param options Object of options to modify functionality * - disableScrollMask Boolean of whether or not to create a scroll mask element or * use the passed parent element. */ disableScrollAround: function(element, parent, options) { options = options || {}; $mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0); $mdUtil.disableScrollAround._count++; if ($mdUtil.disableScrollAround._restoreScroll) { return $mdUtil.disableScrollAround._restoreScroll; } var body = $document[0].body; var restoreBody = disableBodyScroll(); var restoreElement = disableElementScroll(parent); return $mdUtil.disableScrollAround._restoreScroll = function() { if (--$mdUtil.disableScrollAround._count <= 0) { restoreBody(); restoreElement(); delete $mdUtil.disableScrollAround._restoreScroll; } }; /** * Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking, * and wheel events */ function disableElementScroll(element) { element = angular.element(element || body); var scrollMask; if (options.disableScrollMask) { scrollMask = element; } else { scrollMask = angular.element( '
' + '
' + '
'); element.append(scrollMask); } scrollMask.on('wheel', preventDefault); scrollMask.on('touchmove', preventDefault); return function restoreElementScroll() { scrollMask.off('wheel'); scrollMask.off('touchmove'); if (!options.disableScrollMask && scrollMask[0].parentNode ) { scrollMask[0].parentNode.removeChild(scrollMask[0]); } }; function preventDefault(e) { e.preventDefault(); } } // Converts the body to a position fixed block and translate it to the proper scroll position function disableBodyScroll() { var documentElement = $document[0].documentElement; var prevDocumentStyle = documentElement.style.cssText || ''; var prevBodyStyle = body.style.cssText || ''; var viewportTop = $mdUtil.getViewportTop(); var clientWidth = body.clientWidth; var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1; // Scroll may be set on element (for example by overflow-y: scroll) // but Chrome is reporting the scrollTop position always on . // scrollElement will allow to restore the scrollTop position to proper target. var scrollElement = documentElement.scrollTop > 0 ? documentElement : body; if (hasVerticalScrollbar) { angular.element(body).css({ position: 'fixed', width: '100%', top: -viewportTop + 'px' }); } if (body.clientWidth < clientWidth) { body.style.overflow = 'hidden'; } // This should be applied after the manipulation to the body, because // adding a scrollbar can potentially resize it, causing the measurement // to change. if (hasVerticalScrollbar) { documentElement.style.overflowY = 'scroll'; } return function restoreScroll() { // Reset the inline style CSS to the previous. body.style.cssText = prevBodyStyle; documentElement.style.cssText = prevDocumentStyle; // The scroll position while being fixed scrollElement.scrollTop = viewportTop; }; } }, enableScrolling: function() { var restoreFn = this.disableScrollAround._restoreScroll; restoreFn && restoreFn(); }, floatingScrollbars: function() { if (this.floatingScrollbars.cached === undefined) { var tempNode = angular.element('
').css({ width: '100%', 'z-index': -1, position: 'absolute', height: '35px', 'overflow-y': 'scroll' }); tempNode.children().css('height', '60px'); $document[0].body.appendChild(tempNode[0]); this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth); tempNode.remove(); } return this.floatingScrollbars.cached; }, // Mobile safari only allows you to set focus in click event listeners... forceFocus: function(element) { var node = element[0] || element; document.addEventListener('click', function focusOnClick(ev) { if (ev.target === node && ev.$focus) { node.focus(); ev.stopImmediatePropagation(); ev.preventDefault(); node.removeEventListener('click', focusOnClick); } }, true); var newEvent = document.createEvent('MouseEvents'); newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0, false, false, false, false, 0, null); newEvent.$material = true; newEvent.$focus = true; node.dispatchEvent(newEvent); }, /** * facade to build md-backdrop element with desired styles * NOTE: Use $compile to trigger backdrop postLink function */ createBackdrop: function(scope, addClass) { return $compile($mdUtil.supplant('', [addClass]))(scope); }, /** * supplant() method from Crockford's `Remedial Javascript` * Equivalent to use of $interpolate; without dependency on * interpolation symbols and scope. Note: the '{}' can * be property names, property chains, or array indices. */ supplant: function(template, values, pattern) { pattern = pattern || /\{([^\{\}]*)\}/g; return template.replace(pattern, function(a, b) { var p = b.split('.'), r = values; try { for (var s in p) { if (p.hasOwnProperty(s) ) { r = r[p[s]]; } } } catch (e) { r = a; } return (typeof r === 'string' || typeof r === 'number') ? r : a; }); }, fakeNgModel: function() { return { $fake: true, $setTouched: angular.noop, $setViewValue: function(value) { this.$viewValue = value; this.$render(value); this.$viewChangeListeners.forEach(function(cb) { cb(); }); }, $isEmpty: function(value) { return ('' + value).length === 0; }, $parsers: [], $formatters: [], $viewChangeListeners: [], $render: angular.noop }; }, // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs // @param invokeApply should the $timeout trigger $digest() dirty checking debounce: function(func, wait, scope, invokeApply) { var timer; return function debounced() { var context = scope, args = Array.prototype.slice.call(arguments); $timeout.cancel(timer); timer = $timeout(function() { timer = undefined; func.apply(context, args); }, wait || 10, invokeApply); }; }, // Returns a function that can only be triggered every `delay` milliseconds. // In other words, the function will not be called unless it has been more // than `delay` milliseconds since the last call. throttle: function throttle(func, delay) { var recent; return function throttled() { var context = this; var args = arguments; var now = $mdUtil.now(); if (!recent || (now - recent > delay)) { func.apply(context, args); recent = now; } }; }, /** * Measures the number of milliseconds taken to run the provided callback * function. Uses a high-precision timer if available. */ time: function time(cb) { var start = $mdUtil.now(); cb(); return $mdUtil.now() - start; }, /** * Create an implicit getter that caches its `getter()` * lookup value */ valueOnUse : function (scope, key, getter) { var value = null, args = Array.prototype.slice.call(arguments); var params = (args.length > 3) ? args.slice(3) : [ ]; Object.defineProperty(scope, key, { get: function () { if (value === null) value = getter.apply(scope, params); return value; } }); }, /** * Get a unique ID. * * @returns {string} an unique numeric string */ nextUid: function() { return '' + nextUniqueId++; }, // Stop watchers and events from firing on a scope without destroying it, // by disconnecting it from its parent and its siblings' linked lists. disconnectScope: function disconnectScope(scope) { if (!scope) return; // we can't destroy the root scope or a scope that has been already destroyed if (scope.$root === scope) return; if (scope.$$destroyed) return; var parent = scope.$parent; scope.$$disconnected = true; // See Scope.$destroy if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling; if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling; if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; scope.$$nextSibling = scope.$$prevSibling = null; }, // Undo the effects of disconnectScope above. reconnectScope: function reconnectScope(scope) { if (!scope) return; // we can't disconnect the root node or scope already disconnected if (scope.$root === scope) return; if (!scope.$$disconnected) return; var child = scope; var parent = child.$parent; child.$$disconnected = false; // See Scope.$new for this logic... child.$$prevSibling = parent.$$childTail; if (parent.$$childHead) { parent.$$childTail.$$nextSibling = child; parent.$$childTail = child; } else { parent.$$childHead = parent.$$childTail = child; } }, /* * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName * * @param el Element to start walking the DOM from * @param check Either a string or a function. If a string is passed, it will be evaluated against * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of * the parents and will use the return value to determine whether the node is a match. * @param onlyParent Only start checking from the parent element, not `el`. */ getClosest: function getClosest(el, validateWith, onlyParent) { if ( angular.isString(validateWith) ) { var tagName = validateWith.toUpperCase(); validateWith = function(el) { return el.nodeName.toUpperCase() === tagName; }; } if (el instanceof angular.element) el = el[0]; if (onlyParent) el = el.parentNode; if (!el) return null; do { if (validateWith(el)) { return el; } } while (el = el.parentNode); return null; }, /** * Build polyfill for the Node.contains feature (if needed) */ elementContains: function(node, child) { var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains); var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) { // compares the positions of two nodes and returns a bitmask return (node === child) || !!(this.compareDocumentPosition(arg) & 16) }); return findFn(child); }, /** * Functional equivalent for $element.filter(‘md-bottom-sheet’) * useful with interimElements where the element and its container are important... * * @param {[]} elements to scan * @param {string} name of node to find (e.g. 'md-dialog') * @param {boolean=} optional flag to allow deep scans; defaults to 'false'. * @param {boolean=} optional flag to enable log warnings; defaults to false */ extractElementByName: function(element, nodeName, scanDeep, warnNotFound) { var found = scanTree(element); if (!found && !!warnNotFound) { $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) ); } return angular.element(found || element); /** * Breadth-First tree scan for element with matching `nodeName` */ function scanTree(element) { return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null); } /** * Case-insensitive scan of current elements only (do not descend). */ function scanLevel(element) { if ( element ) { for (var i = 0, len = element.length; i < len; i++) { if (element[i].nodeName.toLowerCase() === nodeName) { return element[i]; } } } return null; } /** * Scan children of specified node */ function scanChildren(element) { var found; if ( element ) { for (var i = 0, len = element.length; i < len; i++) { var target = element[i]; if ( !found ) { for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) { found = found || scanTree([target.childNodes[j]]); } } } } return found; } }, /** * Give optional properties with no value a boolean true if attr provided or false otherwise */ initOptionalProperties: function(scope, attr, defaults) { defaults = defaults || {}; angular.forEach(scope.$$isolateBindings, function(binding, key) { if (binding.optional && angular.isUndefined(scope[key])) { var attrIsDefined = angular.isDefined(attr[binding.attrName]); scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined; } }); }, /** * Alternative to $timeout calls with 0 delay. * nextTick() coalesces all calls within a single frame * to minimize $digest thrashing * * @param callback * @param digest * @returns {*} */ nextTick: function(callback, digest, scope) { //-- grab function reference for storing state details var nextTick = $mdUtil.nextTick; var timeout = nextTick.timeout; var queue = nextTick.queue || []; //-- add callback to the queue queue.push({scope: scope, callback: callback}); //-- set default value for digest if (digest == null) digest = true; //-- store updated digest/queue values nextTick.digest = nextTick.digest || digest; nextTick.queue = queue; //-- either return existing timeout or create a new one return timeout || (nextTick.timeout = $timeout(processQueue, 0, false)); /** * Grab a copy of the current queue * Clear the queue for future use * Process the existing queue * Trigger digest if necessary */ function processQueue() { var queue = nextTick.queue; var digest = nextTick.digest; nextTick.queue = []; nextTick.timeout = null; nextTick.digest = false; queue.forEach(function(queueItem) { var skip = queueItem.scope && queueItem.scope.$$destroyed; if (!skip) { queueItem.callback(); } }); if (digest) $rootScope.$digest(); } }, /** * Processes a template and replaces the start/end symbols if the application has * overriden them. * * @param template The template to process whose start/end tags may be replaced. * @returns {*} */ processTemplate: function(template) { if (usesStandardSymbols) { return template; } else { if (!template || !angular.isString(template)) return template; return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); } }, /** * Scan up dom hierarchy for enabled parent; */ getParentWithPointerEvents: function (element) { var parent = element.parent(); // jqLite might return a non-null, but still empty, parent; so check for parent and length while (hasComputedStyle(parent, 'pointer-events', 'none')) { parent = parent.parent(); } return parent; }, getNearestContentElement: function (element) { var current = element.parent()[0]; // Look for the nearest parent md-content, stopping at the rootElement. while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') { current = current.parentNode; } return current; }, /** * Checks if the current browser is natively supporting the `sticky` position. * @returns {string} supported sticky property name */ checkStickySupport: function() { var stickyProp; var testEl = angular.element('
'); $document[0].body.appendChild(testEl[0]); var stickyProps = ['sticky', '-webkit-sticky']; for (var i = 0; i < stickyProps.length; ++i) { testEl.css({ position: stickyProps[i], top: 0, 'z-index': 2 }); if (testEl.css('position') == stickyProps[i]) { stickyProp = stickyProps[i]; break; } } testEl.remove(); return stickyProp; }, /** * Parses an attribute value, mostly a string. * By default checks for negated values and returns `false´ if present. * Negated values are: (native falsy) and negative strings like: * `false` or `0`. * @param value Attribute value which should be parsed. * @param negatedCheck When set to false, won't check for negated values. * @returns {boolean} */ parseAttributeBoolean: function(value, negatedCheck) { return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); }, hasComputedStyle: hasComputedStyle, /** * Returns true if the parent form of the element has been submitted. * * @param element An AngularJS or HTML5 element. * * @returns {boolean} */ isParentFormSubmitted: function(element) { var parent = $mdUtil.getClosest(element, 'form'); var form = parent ? angular.element(parent).controller('form') : null; return form ? form.$submitted : false; }, /** * Animate the requested element's scrollTop to the requested scrollPosition with basic easing. * * @param {!HTMLElement} element The element to scroll. * @param {number} scrollEnd The new/final scroll position. * @param {number=} duration Duration of the scroll. Default is 1000ms. */ animateScrollTo: function(element, scrollEnd, duration) { var scrollStart = element.scrollTop; var scrollChange = scrollEnd - scrollStart; var scrollingDown = scrollStart < scrollEnd; var startTime = $mdUtil.now(); $$rAF(scrollChunk); function scrollChunk() { var newPosition = calculateNewPosition(); element.scrollTop = newPosition; if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) { $$rAF(scrollChunk); } } function calculateNewPosition() { var easeDuration = duration || 1000; var currentTime = $mdUtil.now() - startTime; return ease(currentTime, scrollStart, scrollChange, easeDuration); } function ease(currentTime, start, change, duration) { // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump // straight to the proper position if (currentTime > duration) { return start + change; } var ts = (currentTime /= duration) * currentTime; var tc = ts * currentTime; return start + change * (-2 * tc + 3 * ts); } }, /** * Provides an easy mechanism for removing duplicates from an array. * * var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]; * * $mdUtil.uniq(myArray) => [1, 2, 3, 4] * * @param {array} array The array whose unique values should be returned. * * @returns {array} A copy of the array containing only unique values. */ uniq: function(array) { if (!array) { return; } return array.filter(function(value, index, self) { return self.indexOf(value) === index; }); } }; // Instantiate other namespace utility methods $mdUtil.dom.animator = $$mdAnimate($mdUtil); return $mdUtil; function getNode(el) { return el[0] || el; } } /* * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. */ angular.element.prototype.focus = angular.element.prototype.focus || function() { if (this.length) { this[0].focus(); } return this; }; angular.element.prototype.blur = angular.element.prototype.blur || function() { if (this.length) { this[0].blur(); } return this; }; })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.aria * @description * Aria Expectations for AngularJS Material components. */ MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"]; angular .module('material.core') .provider('$mdAria', MdAriaProvider); /** * @ngdoc service * @name $mdAriaProvider * @module material.core.aria * * @description * * Modify options of the `$mdAria` service, which will be used by most of the AngularJS Material * components. * * You are able to disable `$mdAria` warnings, by using the following markup. * * * app.config(function($mdAriaProvider) { * // Globally disables all ARIA warnings. * $mdAriaProvider.disableWarnings(); * }); * * */ function MdAriaProvider() { var config = { /** Whether we should show ARIA warnings in the console if labels are missing on the element */ showWarnings: true }; return { disableWarnings: disableWarnings, $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) { return MdAriaService.apply(config, arguments); }] }; /** * @ngdoc method * @name $mdAriaProvider#disableWarnings * @description Disables all ARIA warnings generated by AngularJS Material. */ function disableWarnings() { config.showWarnings = false; } } /* * @ngInject */ function MdAriaService($$rAF, $log, $window, $interpolate) { // Load the showWarnings option from the current context and store it inside of a scope variable, // because the context will be probably lost in some function calls. var showWarnings = this.showWarnings; return { expect: expect, expectAsync: expectAsync, expectWithText: expectWithText, expectWithoutText: expectWithoutText, getText: getText, hasAriaLabel: hasAriaLabel, parentHasAriaLabel: parentHasAriaLabel }; /** * Check if expected attribute has been specified on the target element or child * @param element * @param attrName * @param {optional} defaultValue What to set the attr to if no value is found */ function expect(element, attrName, defaultValue) { var node = angular.element(element)[0] || element; // if node exists and neither it nor its children have the attribute if (node && ((!node.hasAttribute(attrName) || node.getAttribute(attrName).length === 0) && !childHasAttribute(node, attrName))) { defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : ''; if (defaultValue.length) { element.attr(attrName, defaultValue); } else if (showWarnings) { $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node); } } } function expectAsync(element, attrName, defaultValueGetter) { // Problem: when retrieving the element's contents synchronously to find the label, // the text may not be defined yet in the case of a binding. // There is a higher chance that a binding will be defined if we wait one frame. $$rAF(function() { expect(element, attrName, defaultValueGetter()); }); } function expectWithText(element, attrName) { var content = getText(element) || ""; var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; if (hasBinding) { expectAsync(element, attrName, function() { return getText(element); }); } else { expect(element, attrName, content); } } function expectWithoutText(element, attrName) { var content = getText(element); var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; if ( !hasBinding && !content) { expect(element, attrName, content); } } function getText(element) { element = element[0] || element; var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); var text = ''; var node; while (node = walker.nextNode()) { if (!isAriaHiddenNode(node)) { text += node.textContent; } } return text.trim() || ''; function isAriaHiddenNode(node) { while (node.parentNode && (node = node.parentNode) !== element) { if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') { return true; } } } } function childHasAttribute(node, attrName) { var hasChildren = node.hasChildNodes(), hasAttr = false; function isHidden(el) { var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el); return (style.display === 'none'); } if (hasChildren) { var children = node.childNodes; for (var i=0; i < children.length; i++) { var child = children[i]; if (child.nodeType === 1 && child.hasAttribute(attrName)) { if (!isHidden(child)) { hasAttr = true; } } } } return hasAttr; } /** * Check if expected element has aria label attribute * @param element */ function hasAriaLabel(element) { var node = angular.element(element)[0] || element; /* Check if compatible node type (ie: not HTML Document node) */ if (!node.hasAttribute) { return false; } /* Check label or description attributes */ return node.hasAttribute('aria-label') || node.hasAttribute('aria-labelledby') || node.hasAttribute('aria-describedby'); } /** * Check if expected element's parent has aria label attribute and has valid role and tagName * @param element * @param {optional} level Number of levels deep search should be performed */ function parentHasAriaLabel(element, level) { level = level || 1; var node = angular.element(element)[0] || element; if (!node.parentNode) { return false; } if (performCheck(node.parentNode)) { return true; } level--; if (level) { return parentHasAriaLabel(node.parentNode, level); } return false; function performCheck(parentNode) { if (!hasAriaLabel(parentNode)) { return false; } /* Perform role blacklist check */ if (parentNode.hasAttribute('role')) { switch(parentNode.getAttribute('role').toLowerCase()) { case 'command': case 'definition': case 'directory': case 'grid': case 'list': case 'listitem': case 'log': case 'marquee': case 'menu': case 'menubar': case 'note': case 'presentation': case 'separator': case 'scrollbar': case 'status': case 'tablist': return false; } } /* Perform tagName blacklist check */ switch(parentNode.tagName.toLowerCase()) { case 'abbr': case 'acronym': case 'address': case 'applet': case 'audio': case 'b': case 'bdi': case 'bdo': case 'big': case 'blockquote': case 'br': case 'canvas': case 'caption': case 'center': case 'cite': case 'code': case 'col': case 'data': case 'dd': case 'del': case 'dfn': case 'dir': case 'div': case 'dl': case 'em': case 'embed': case 'fieldset': case 'figcaption': case 'font': case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': case 'hgroup': case 'html': case 'i': case 'ins': case 'isindex': case 'kbd': case 'keygen': case 'label': case 'legend': case 'li': case 'map': case 'mark': case 'menu': case 'object': case 'ol': case 'output': case 'pre': case 'presentation': case 'q': case 'rt': case 'ruby': case 'samp': case 'small': case 'source': case 'span': case 'status': case 'strike': case 'strong': case 'sub': case 'sup': case 'svg': case 'tbody': case 'td': case 'th': case 'thead': case 'time': case 'tr': case 'track': case 'tt': case 'ul': case 'var': return false; } return true; } } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.compiler * @description * AngularJS Material template and element compiler. */ angular .module('material.core') .provider('$mdCompiler', MdCompilerProvider); /** * @ngdoc provider * @name $mdCompilerProvider * * @description */ /** * @ngdoc service * @name $mdCompiler * @module material.core.compiler * @description * The $mdCompiler service is an abstraction of AngularJS's compiler, that allows developers * to easily compile an element with options like in a Directive Definition Object. * * > The compiler powers a lot of components inside of AngularJS Material. * > Like the `$mdPanel` or `$mdDialog`. * * @usage * * Basic Usage with a template * * * $mdCompiler.compile({ * templateUrl: 'modal.html', * controller: 'ModalCtrl', * locals: { * modal: myModalInstance; * } * }).then(function (compileData) { * compileData.element; // Compiled DOM element * compileData.link(myScope); // Instantiate controller and link element to scope. * }); * * * Example with a content element * * * * // Create a virtual element and link it manually. * // The compiler doesn't need to recompile the element each time. * var myElement = $compile('Test')(myScope); * * $mdCompiler.compile({ * contentElement: myElement * }).then(function (compileData) { * compileData.element // Content Element (same as above) * compileData.link // This does nothing when using a contentElement. * }); * * * > Content Element is a significant performance improvement when the developer already knows that the * > compiled element will be always the same and the scope will not change either. * * The `contentElement` option also supports DOM elements which will be temporary removed and restored * at its old position. * * * var domElement = document.querySelector('#myElement'); * * $mdCompiler.compile({ * contentElement: myElement * }).then(function (compileData) { * compileData.element // Content Element (same as above) * compileData.link // This does nothing when using a contentElement. * }); * * * The `$mdCompiler` can also query for the element in the DOM itself. * * * $mdCompiler.compile({ * contentElement: '#myElement' * }).then(function (compileData) { * compileData.element // Content Element (same as above) * compileData.link // This does nothing when using a contentElement. * }); * * */ MdCompilerProvider.$inject = ['$compileProvider']; function MdCompilerProvider($compileProvider) { var provider = this; /** * @name $mdCompilerProvider#respectPreAssignBindingsEnabled * * @param {boolean=} respected update the respectPreAssignBindingsEnabled state if provided, otherwise just return * the current Material preAssignBindingsEnabled state * @returns {*} current value if used as getter or itself (chaining) if used as setter * * @description * Call this method to enable/disable whether Material-specific (dialogs/toasts) controllers respect the AngularJS * `$compile.preAssignBindingsEnabled` flag. Note that this doesn't affect directives/components created via * regular AngularJS methods which constitute most Material & user-created components. * * @see [AngularJS documentation for `$compile.preAssignBindingsEnabled` * ](https://code.angularjs.org/1.6.4/docs/api/ng/provider/$compileProvider#preAssignBindingsEnabled) * for more information. * * If disabled (false), the compiler assigns the value of each of the bindings to the * properties of the controller object before the constructor of this object is called. * * If enabled (true) the behavior depends on the AngularJS version used: * * - `<1.5.10` - bindings are pre-assigned * - `>=1.5.10 <1.7` - behaves like set to whatever `$compileProvider.preAssignBindingsEnabled()` reports; if * the `$compileProvider` flag wasn't set manually, it defaults to pre-assigning bindings * with AngularJS `1.5.x` & to calling the constructor first with AngularJS `1.6.x`. * - `>=1.7` - the compiler calls the constructor first before assigning bindings * * The default value is `false` but will change to `true` in AngularJS Material 1.2. * * It is recommended to set this flag to `true` in AngularJS Material 1.1.x; the only reason it's not set that way * by default is backwards compatibility. Not setting the flag to `true` when * `$compileProvider.preAssignBindingsEnabled()` is set to `false` (i.e. default behavior in AngularJS 1.6.0 or newer) * makes it hard to unit test Material Dialog/Toast controllers using the `$controller` helper as it always follows * the `$compileProvider.preAssignBindingsEnabled()` value. */ // TODO change it to `true` in Material 1.2. var respectPreAssignBindingsEnabled = false; this.respectPreAssignBindingsEnabled = function(respected) { if (angular.isDefined(respected)) { respectPreAssignBindingsEnabled = respected; return this; } return respectPreAssignBindingsEnabled; }; /** * @description * This function returns `true` if Material-specific (dialogs/toasts) controllers have bindings pre-assigned in * controller constructors and `false` otherwise. * * Note that this doesn't affect directives/components created via regular AngularJS methods which constitute most * Material & user-created components; their behavior can be checked via `$compileProvider.preAssignBindingsEnabled()` * in AngularJS `>=1.5.10 <1.7.0`. * * @returns {*} current preAssignBindingsEnabled state */ function getPreAssignBindingsEnabled() { if (!respectPreAssignBindingsEnabled) { // respectPreAssignBindingsEnabled === false // We're ignoring the AngularJS `$compileProvider.preAssignBindingsEnabled()` value in this case. return true; } // respectPreAssignBindingsEnabled === true if (typeof $compileProvider.preAssignBindingsEnabled === 'function') { return $compileProvider.preAssignBindingsEnabled(); } // Flag respected but not present => apply logic based on AngularJS version used. if (angular.version.major === 1 && angular.version.minor < 6) { // AngularJS <1.5.10 return true; } // AngularJS >=1.7.0 return false; } this.$get = ["$q", "$templateRequest", "$injector", "$compile", "$controller", function($q, $templateRequest, $injector, $compile, $controller) { return new MdCompilerService($q, $templateRequest, $injector, $compile, $controller); }]; function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) { /** @private @const {!angular.$q} */ this.$q = $q; /** @private @const {!angular.$templateRequest} */ this.$templateRequest = $templateRequest; /** @private @const {!angular.$injector} */ this.$injector = $injector; /** @private @const {!angular.$compile} */ this.$compile = $compile; /** @private @const {!angular.$controller} */ this.$controller = $controller; } /** * @ngdoc method * @name $mdCompiler#compile * @description * * A method to compile a HTML template with the AngularJS compiler. * The `$mdCompiler` is wrapper around the AngularJS compiler and provides extra functionality * like controller instantiation or async resolves. * * @param {!Object} options An options object, with the following properties: * * - `controller` - `{string|Function}` Controller fn that should be associated with * newly created scope or the name of a registered controller if passed as a string. * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be * published to scope under the `controllerAs` name. * - `contentElement` - `{string|Element}`: Instead of using a template, which will be * compiled each time, you can also use a DOM element.
* - `template` - `{string=}` An html template as a string. * - `templateUrl` - `{string=}` A path to an html template. * - `transformTemplate` - `{function(template)=}` A function which transforms the template after * it is loaded. It will be given the template string as a parameter, and should * return a a new string representing the transformed template. * - `resolve` - `{Object.=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, the compiler * will wait for them all to be resolved, or if one is rejected before the controller is * instantiated `compile()` will fail.. * * `key` - `{string}`: a name of a dependency to be injected into the controller. * * `factory` - `{string|function}`: If `string` then it is an alias for a service. * Otherwise if function, then it is injected and the return value is treated as the * dependency. If the result is a promise, it is resolved before its value is * injected into the controller. * * @returns {Object} promise A promise, which will be resolved with a `compileData` object. * `compileData` has the following properties: * * - `element` - `{element}`: an uncompiled element matching the provided template. * - `link` - `{function(scope)}`: A link function, which, when called, will compile * the element and instantiate the provided controller (if given). * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is * called. If `bindToController` is true, they will be coppied to the ctrl instead * */ MdCompilerService.prototype.compile = function(options) { if (options.contentElement) { return this._prepareContentElement(options); } else { return this._compileTemplate(options); } }; /** * Instead of compiling any template, the compiler just fetches an existing HTML element from the DOM and * provides a restore function to put the element back it old DOM position. * @param {!Object} options Options to be used for the compiler. */ MdCompilerService.prototype._prepareContentElement = function(options) { var contentElement = this._fetchContentElement(options); return this.$q.resolve({ element: contentElement.element, cleanup: contentElement.restore, locals: {}, link: function() { return contentElement.element; } }); }; /** * Compiles a template by considering all options and waiting for all resolves to be ready. * @param {!Object} options Compile options * @returns {!Object} Compile data with link function. */ MdCompilerService.prototype._compileTemplate = function(options) { var self = this; var templateUrl = options.templateUrl; var template = options.template || ''; var resolve = angular.extend({}, options.resolve); var locals = angular.extend({}, options.locals); var transformTemplate = options.transformTemplate || angular.identity; // Take resolve values and invoke them. // Resolves can either be a string (value: 'MyRegisteredAngularConst'), // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) angular.forEach(resolve, function(value, key) { if (angular.isString(value)) { resolve[key] = self.$injector.get(value); } else { resolve[key] = self.$injector.invoke(value); } }); // Add the locals, which are just straight values to inject // eg locals: { three: 3 }, will inject three into the controller angular.extend(resolve, locals); if (templateUrl) { resolve.$$ngTemplate = this.$templateRequest(templateUrl); } else { resolve.$$ngTemplate = this.$q.when(template); } // Wait for all the resolves to finish if they are promises return this.$q.all(resolve).then(function(locals) { var template = transformTemplate(locals.$$ngTemplate, options); var element = options.element || angular.element('
').html(template.trim()).contents(); return self._compileElement(locals, element, options); }); }; /** * Method to compile an element with the given options. * @param {!Object} locals Locals to be injected to the controller if present * @param {!JQLite} element Element to be compiled and linked * @param {!Object} options Options to be used for linking. * @returns {!Object} Compile data with link function. */ MdCompilerService.prototype._compileElement = function(locals, element, options) { var self = this; var ngLinkFn = this.$compile(element); var compileData = { element: element, cleanup: element.remove.bind(element), locals: locals, link: linkFn }; function linkFn(scope) { locals.$scope = scope; // Instantiate controller if the developer provided one. if (options.controller) { var injectLocals = angular.extend({}, locals, { $element: element }); // Create the specified controller instance. var ctrl = self._createController(options, injectLocals, locals); // Unique identifier for AngularJS Route ngView controllers. element.data('$ngControllerController', ctrl); element.children().data('$ngControllerController', ctrl); // Expose the instantiated controller to the compile data compileData.controller = ctrl; } // Invoke the AngularJS $compile link function. return ngLinkFn(scope); } return compileData; }; /** * Creates and instantiates a new controller with the specified options. * @param {!Object} options Options that include the controller * @param {!Object} injectLocals Locals to to be provided in the controller DI. * @param {!Object} locals Locals to be injected to the controller. * @returns {!Object} Created controller instance. */ MdCompilerService.prototype._createController = function(options, injectLocals, locals) { var invokeCtrl = this.$controller(options.controller, injectLocals, true, options.controllerAs); if (getPreAssignBindingsEnabled() && options.bindToController) { angular.extend(invokeCtrl.instance, locals); } // Instantiate and initialize the specified controller. var ctrl = invokeCtrl(); if (!getPreAssignBindingsEnabled() && options.bindToController) { angular.extend(invokeCtrl.instance, locals); } // Call the $onInit hook if it's present on the controller. angular.isFunction(ctrl.$onInit) && ctrl.$onInit(); return ctrl; }; /** * Fetches an element removing it from the DOM and using it temporary for the compiler. * Elements which were fetched will be restored after use. * @param {!Object} options Options to be used for the compilation. * @returns {{element: !JQLite, restore: !Function}} */ MdCompilerService.prototype._fetchContentElement = function(options) { var contentEl = options.contentElement; var restoreFn = null; if (angular.isString(contentEl)) { contentEl = document.querySelector(contentEl); restoreFn = createRestoreFn(contentEl); } else { contentEl = contentEl[0] || contentEl; // When the element is visible in the DOM, then we restore it at close of the dialog. // Otherwise it will be removed from the DOM after close. if (document.contains(contentEl)) { restoreFn = createRestoreFn(contentEl); } else { restoreFn = function() { if (contentEl.parentNode) { contentEl.parentNode.removeChild(contentEl); } } } } return { element: angular.element(contentEl), restore: restoreFn }; function createRestoreFn(element) { var parent = element.parentNode; var nextSibling = element.nextElementSibling; return function() { if (!nextSibling) { // When the element didn't had any sibling, then it can be simply appended to the // parent, because it plays no role, which index it had before. parent.appendChild(element); } else { // When the element had a sibling, which marks the previous position of the element // in the DOM, we insert it correctly before the sibling, to have the same index as // before. parent.insertBefore(element, nextSibling); } } } }; } })(); (function(){ "use strict"; MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];var HANDLERS = {}; /* The state of the current 'pointer' * The pointer represents the state of the current touch. * It contains normalized x and y coordinates from DOM events, * as well as other information abstracted from the DOM. */ var pointer, lastPointer, forceSkipClickHijack = false, maxClickDistance = 6; /** * The position of the most recent click if that click was on a label element. * @type {{x: number, y: number}?} */ var lastLabelClickPos = null; // Used to attach event listeners once when multiple ng-apps are running. var isInitialized = false; angular .module('material.core.gestures', [ ]) .provider('$mdGesture', MdGestureProvider) .factory('$$MdGestureHandler', MdGestureHandler) .run(attachToDocument ); /** * @ngdoc service * @name $mdGestureProvider * @module material.core.gestures * * @description * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile * devices. * You can also change max click distance (6px by default) if you have issues on some touch screens. * * * app.config(function($mdGestureProvider) { * * // For mobile devices without jQuery loaded, do not * // intercept click events during the capture phase. * $mdGestureProvider.skipClickHijack(); * * // If hijcacking clicks, change default 6px click distance * $mdGestureProvider.setMaxClickDistance(12); * * }); * * */ function MdGestureProvider() { } MdGestureProvider.prototype = { // Publish access to setter to configure a variable BEFORE the // $mdGesture service is instantiated... skipClickHijack: function() { return forceSkipClickHijack = true; }, setMaxClickDistance: function(clickDistance) { maxClickDistance = parseInt(clickDistance); }, /** * $get is used to build an instance of $mdGesture * @ngInject */ $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { return new MdGesture($$MdGestureHandler, $$rAF, $timeout); }] }; /** * MdGesture factory construction function * @ngInject */ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { var userAgent = navigator.userAgent || navigator.vendor || window.opera; var isIos = userAgent.match(/ipad|iphone|ipod/i); var isAndroid = userAgent.match(/android/i); var touchActionProperty = getTouchAction(); var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); var self = { handler: addHandler, register: register, // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack }; if (self.isHijackingClicks) { self.handler('click', { options: { maxDistance: maxClickDistance }, onEnd: checkDistanceAndEmit('click') }); self.handler('focus', { options: { maxDistance: maxClickDistance }, onEnd: function(ev, pointer) { if (pointer.distance < this.state.options.maxDistance && canFocus(ev.target)) { this.dispatchEvent(ev, 'focus', pointer); ev.target.focus(); } } }); self.handler('mouseup', { options: { maxDistance: maxClickDistance }, onEnd: checkDistanceAndEmit('mouseup') }); self.handler('mousedown', { onStart: function(ev) { this.dispatchEvent(ev, 'mousedown'); } }); } function checkDistanceAndEmit(eventName) { return function(ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { this.dispatchEvent(ev, eventName, pointer); } }; } /* * Register an element to listen for a handler. * This allows an element to override the default options for a handler. * Additionally, some handlers like drag and hold only dispatch events if * the domEvent happens inside an element that's registered to listen for these events. * * @see GestureHandler for how overriding of default options works. * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) */ function register(element, handlerName, options) { var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; if (!handler) { throw new Error('Failed to register element with handler ' + handlerName + '. ' + 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); } return handler.registerElement(element, options); } /* * add a handler to $mdGesture. see below. */ function addHandler(name, definition) { var handler = new $$MdGestureHandler(name); angular.extend(handler, definition); HANDLERS[name] = handler; return self; } /* * Register handlers. These listen to touch/start/move events, interpret them, * and dispatch gesture events depending on options & conditions. These are all * instances of GestureHandler. * @see GestureHandler */ return self /* * The press handler dispatches an event on touchdown/touchend. * It's a simple abstraction of touch/mouse/pointer start and end. */ .handler('press', { onStart: function (ev, pointer) { this.dispatchEvent(ev, '$md.pressdown'); }, onEnd: function (ev, pointer) { this.dispatchEvent(ev, '$md.pressup'); } }) /* * The hold handler dispatches an event if the user keeps their finger within * the same area for ms. * The hold handler will only run if a parent of the touch target is registered * to listen for hold events through $mdGesture.register() */ .handler('hold', { options: { maxDistance: 6, delay: 500 }, onCancel: function () { $timeout.cancel(this.state.timeout); }, onStart: function (ev, pointer) { // For hold, require a parent to be registered with $mdGesture.register() // Because we prevent scroll events, this is necessary. if (!this.state.registeredParent) return this.cancel(); this.state.pos = {x: pointer.x, y: pointer.y}; this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { this.dispatchEvent(ev, '$md.hold'); this.cancel(); //we're done! }), this.state.options.delay, false); }, onMove: function (ev, pointer) { // Don't scroll while waiting for hold. // If we don't preventDefault touchmove events here, Android will assume we don't // want to listen to anymore touch events. It will start scrolling and stop sending // touchmove events. if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); // If the user moves greater than pixels, stop the hold timer // set in onStart var dx = this.state.pos.x - pointer.x; var dy = this.state.pos.y - pointer.y; if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { this.cancel(); } }, onEnd: function () { this.onCancel(); } }) /* * The drag handler dispatches a drag event if the user holds and moves his finger greater than * px in the x or y direction, depending on options.horizontal. * The drag will be cancelled if the user moves his finger greater than * in * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger * * pixels vertically, this handler won't consider the move part of a drag. */ .handler('drag', { options: { minDistance: 6, horizontal: true, cancelMultiplier: 1.5 }, onSetup: function(element, options) { if (touchActionProperty) { // We check for horizontal to be false, because otherwise we would overwrite the default opts. this.oldTouchAction = element[0].style[touchActionProperty]; element[0].style[touchActionProperty] = options.horizontal ? 'pan-y' : 'pan-x'; } }, onCleanup: function(element) { if (this.oldTouchAction) { element[0].style[touchActionProperty] = this.oldTouchAction; } }, onStart: function (ev) { // For drag, require a parent to be registered with $mdGesture.register() if (!this.state.registeredParent) this.cancel(); }, onMove: function (ev, pointer) { var shouldStartDrag, shouldCancel; // Don't scroll while deciding if this touchmove qualifies as a drag event. // If we don't preventDefault touchmove events here, Android will assume we don't // want to listen to anymore touch events. It will start scrolling and stop sending // touchmove events. if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); if (!this.state.dragPointer) { if (this.state.options.horizontal) { shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; } else { shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; } if (shouldStartDrag) { // Create a new pointer representing this drag, starting at this point where the drag started. this.state.dragPointer = makeStartPointer(ev); updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); } else if (shouldCancel) { this.cancel(); } } else { this.dispatchDragMove(ev); } }, // Only dispatch dragmove events every frame; any more is unnecessary dispatchDragMove: $$rAF.throttle(function (ev) { // Make sure the drag didn't stop while waiting for the next frame if (this.state.isRunning) { updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); } }), onEnd: function (ev, pointer) { if (this.state.dragPointer) { updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); } } }) /* * The swipe handler will dispatch a swipe event if, on the end of a touch, * the velocity and distance were high enough. */ .handler('swipe', { options: { minVelocity: 0.65, minDistance: 10 }, onEnd: function (ev, pointer) { var eventType; if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && Math.abs(pointer.distanceX) > this.state.options.minDistance) { eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; this.dispatchEvent(ev, eventType); } else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && Math.abs(pointer.distanceY) > this.state.options.minDistance) { eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; this.dispatchEvent(ev, eventType); } } }); function getTouchAction() { var testEl = document.createElement('div'); var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; for (var i = 0; i < vendorPrefixes.length; i++) { var prefix = vendorPrefixes[i]; var property = prefix ? prefix + 'TouchAction' : 'touchAction'; if (angular.isDefined(testEl.style[property])) { return property; } } } } /** * MdGestureHandler * A GestureHandler is an object which is able to dispatch custom dom events * based on native dom {touch,pointer,mouse}{start,move,end} events. * * A gesture will manage its lifecycle through the start,move,end, and cancel * functions, which are called by native dom events. * * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be * overridden by elements registering through $mdGesture.register() */ function GestureHandler (name) { this.name = name; this.state = {}; } function MdGestureHandler() { var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); GestureHandler.prototype = { options: {}, // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events // differently when jQuery is loaded dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, // These are overridden by the registered handler onSetup: angular.noop, onCleanup: angular.noop, onStart: angular.noop, onMove: angular.noop, onEnd: angular.noop, onCancel: angular.noop, // onStart sets up a new state for the handler, which includes options from the // nearest registered parent element of ev.target. start: function (ev, pointer) { if (this.state.isRunning) return; var parentTarget = this.getNearestParent(ev.target); // Get the options from the nearest registered parent var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; this.state = { isRunning: true, // Override the default options with the nearest registered parent's options options: angular.extend({}, this.options, parentTargetOptions), // Pass in the registered parent node to the state so the onStart listener can use registeredParent: parentTarget }; this.onStart(ev, pointer); }, move: function (ev, pointer) { if (!this.state.isRunning) return; this.onMove(ev, pointer); }, end: function (ev, pointer) { if (!this.state.isRunning) return; this.onEnd(ev, pointer); this.state.isRunning = false; }, cancel: function (ev, pointer) { this.onCancel(ev, pointer); this.state = {}; }, // Find and return the nearest parent element that has been registered to // listen for this handler via $mdGesture.register(element, 'handlerName'). getNearestParent: function (node) { var current = node; while (current) { if ((current.$mdGesture || {})[this.name]) { return current; } current = current.parentNode; } return null; }, // Called from $mdGesture.register when an element registers itself with a handler. // Store the options the user gave on the DOMElement itself. These options will // be retrieved with getNearestParent when the handler starts. registerElement: function (element, options) { var self = this; element[0].$mdGesture = element[0].$mdGesture || {}; element[0].$mdGesture[this.name] = options || {}; element.on('$destroy', onDestroy); self.onSetup(element, options || {}); return onDestroy; function onDestroy() { delete element[0].$mdGesture[self.name]; element.off('$destroy', onDestroy); self.onCleanup(element, options || {}); } } }; return GestureHandler; /* * Dispatch an event with jQuery * TODO: Make sure this sends bubbling events * * @param srcEvent the original DOM touch event that started this. * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') * @param eventPointer the pointer object that matches this event. */ function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { eventPointer = eventPointer || pointer; var eventObj = new angular.element.Event(eventType); eventObj.$material = true; eventObj.pointer = eventPointer; eventObj.srcEvent = srcEvent; angular.extend(eventObj, { clientX: eventPointer.x, clientY: eventPointer.y, screenX: eventPointer.x, screenY: eventPointer.y, pageX: eventPointer.x, pageY: eventPointer.y, ctrlKey: srcEvent.ctrlKey, altKey: srcEvent.altKey, shiftKey: srcEvent.shiftKey, metaKey: srcEvent.metaKey }); angular.element(eventPointer.target).trigger(eventObj); } /* * NOTE: nativeDispatchEvent is very performance sensitive. * @param srcEvent the original DOM touch event that started this. * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') * @param eventPointer the pointer object that matches this event. */ function nativeDispatchEvent(srcEvent, eventType, eventPointer) { eventPointer = eventPointer || pointer; var eventObj; if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) { eventObj = document.createEvent('MouseEvents'); eventObj.initMouseEvent( eventType, true, true, window, srcEvent.detail, eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, srcEvent.button, srcEvent.relatedTarget || null ); } else { eventObj = document.createEvent('CustomEvent'); eventObj.initCustomEvent(eventType, true, true, {}); } eventObj.$material = true; eventObj.pointer = eventPointer; eventObj.srcEvent = srcEvent; eventPointer.target.dispatchEvent(eventObj); } } /** * Attach Gestures: hook document and check shouldHijack clicks * @ngInject */ function attachToDocument( $mdGesture, $$MdGestureHandler ) { // Polyfill document.contains for IE11. // TODO: move to util document.contains || (document.contains = function (node) { return document.body.contains(node); }); if (!isInitialized && $mdGesture.isHijackingClicks ) { /* * If hijack clicks is true, we preventDefault any click that wasn't * sent by AngularJS Material. This is because on older Android & iOS, a false, or 'ghost', * click event will be sent ~400ms after a touchend event happens. * The only way to know if this click is real is to prevent any normal * click events, and add a flag to events sent by material so we know not to prevent those. * * Two exceptions to click events that should be prevented are: * - click events sent by the keyboard (eg form submit) * - events that originate from an Ionic app */ document.addEventListener('click' , clickHijacker , true); document.addEventListener('mouseup' , mouseInputHijacker, true); document.addEventListener('mousedown', mouseInputHijacker, true); document.addEventListener('focus' , mouseInputHijacker, true); isInitialized = true; } function mouseInputHijacker(ev) { var isKeyClick = !ev.clientX && !ev.clientY; if ( !isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev) && (ev.type !== 'mousedown' || (!canFocus(ev.target) && !canFocus(document.activeElement))) ) { ev.preventDefault(); ev.stopPropagation(); } } function clickHijacker(ev) { var isKeyClick = ev.clientX === 0 && ev.clientY === 0; var isSubmitEvent = ev.target && ev.target.type === 'submit'; if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev) && !isSubmitEvent) { ev.preventDefault(); ev.stopPropagation(); lastLabelClickPos = null; } else { lastLabelClickPos = null; if (ev.target.tagName.toLowerCase() == 'label') { lastLabelClickPos = {x: ev.x, y: ev.y}; } } } // Listen to all events to cover all platforms. var START_EVENTS = 'mousedown touchstart pointerdown'; var MOVE_EVENTS = 'mousemove touchmove pointermove'; var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; angular.element(document) .on(START_EVENTS, gestureStart) .on(MOVE_EVENTS, gestureMove) .on(END_EVENTS, gestureEnd) // For testing .on('$$mdGestureReset', function gestureClearCache () { lastPointer = pointer = null; }); /* * When a DOM event happens, run all registered gesture handlers' lifecycle * methods which match the DOM event. * Eg when a 'touchstart' event happens, runHandlers('start') will call and * run `handler.cancel()` and `handler.start()` on all registered handlers. */ function runHandlers(handlerEvent, event) { var handler; for (var name in HANDLERS) { handler = HANDLERS[name]; if( handler instanceof $$MdGestureHandler ) { if (handlerEvent === 'start') { // Run cancel to reset any handlers' state handler.cancel(); } handler[handlerEvent](event, pointer); } } } /* * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) * If it is legitimate, we initiate the pointer state and mark the current pointer's type * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events * won't effect it. */ function gestureStart(ev) { // If we're already touched down, abort if (pointer) return; var now = +Date.now(); // iOS & old android bug: after a touch event, a click event is sent 350 ms later. // If <400ms have passed, don't allow an event of a different type than the previous event if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { return; } pointer = makeStartPointer(ev); runHandlers('start', ev); } /* * If a move event happens of the right type, update the pointer and run all the move handlers. * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. */ function gestureMove(ev) { if (!pointer || !typesMatch(ev, pointer)) return; updatePointerState(ev, pointer); runHandlers('move', ev); } /* * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' */ function gestureEnd(ev) { if (!pointer || !typesMatch(ev, pointer)) return; updatePointerState(ev, pointer); pointer.endTime = +Date.now(); if (ev.type !== 'pointercancel') { runHandlers('end', ev); } lastPointer = pointer; pointer = null; } } // ******************** // Module Functions // ******************** /* * Initiate the pointer. x, y, and the pointer's type. */ function makeStartPointer(ev) { var point = getEventPoint(ev); var startPointer = { startTime: +Date.now(), target: ev.target, // 'p' for pointer events, 'm' for mouse, 't' for touch type: ev.type.charAt(0) }; startPointer.startX = startPointer.x = point.pageX; startPointer.startY = startPointer.y = point.pageY; return startPointer; } /* * return whether the pointer's type matches the event's type. * Eg if a touch event happens but the pointer has a mouse type, return false. */ function typesMatch(ev, pointer) { return ev && pointer && ev.type.charAt(0) === pointer.type; } /** * Gets whether the given event is an input event that was caused by clicking on an * associated label element. * * This is necessary because the browser will, upon clicking on a label element, fire an * *extra* click event on its associated input (if any). mdGesture is able to flag the label * click as with `$material` correctly, but not the second input click. * * In order to determine whether an input event is from a label click, we compare the (x, y) for * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label * click occurs). Unfortunately, there are no event properties that tie the input and the label * together (such as relatedTarget). * * @param {MouseEvent} event * @returns {boolean} */ function isInputEventFromLabelClick(event) { return lastLabelClickPos && lastLabelClickPos.x == event.x && lastLabelClickPos.y == event.y; } /* * Update the given pointer based upon the given DOMEvent. * Distance, velocity, direction, duration, etc */ function updatePointerState(ev, pointer) { var point = getEventPoint(ev); var x = pointer.x = point.pageX; var y = pointer.y = point.pageY; pointer.distanceX = x - pointer.startX; pointer.distanceY = y - pointer.startY; pointer.distance = Math.sqrt( pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY ); pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; pointer.duration = +Date.now() - pointer.startTime; pointer.velocityX = pointer.distanceX / pointer.duration; pointer.velocityY = pointer.distanceY / pointer.duration; } /* * Normalize the point where the DOM event happened whether it's touch or mouse. * @returns point event obj with pageX and pageY on it. */ function getEventPoint(ev) { ev = ev.originalEvent || ev; // support jQuery events return (ev.touches && ev.touches[0]) || (ev.changedTouches && ev.changedTouches[0]) || ev; } /** Checks whether an element can be focused. */ function canFocus(element) { return ( !!element && element.getAttribute('tabindex') != '-1' && !element.hasAttribute('disabled') && ( element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable || ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'].indexOf(element.nodeName) != -1 ) ); } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.interaction * @description * User interaction detection to provide proper accessibility. */ MdInteractionService.$inject = ["$timeout", "$mdUtil"]; angular .module('material.core.interaction', []) .service('$mdInteraction', MdInteractionService); /** * @ngdoc service * @name $mdInteraction * @module material.core.interaction * * @description * * Service which keeps track of the last interaction type and validates them for several browsers. * The service hooks into the document's body and listens for touch, mouse and keyboard events. * * The most recent interaction type can be retrieved by calling the `getLastInteractionType` method. * * Here is an example markup for using the interaction service. * * * var lastType = $mdInteraction.getLastInteractionType(); * * if (lastType === 'keyboard') { * // We only restore the focus for keyboard users. * restoreFocus(); * } * * */ function MdInteractionService($timeout, $mdUtil) { this.$timeout = $timeout; this.$mdUtil = $mdUtil; this.bodyElement = angular.element(document.body); this.isBuffering = false; this.bufferTimeout = null; this.lastInteractionType = null; this.lastInteractionTime = null; // Type Mappings for the different events // There will be three three interaction types // `keyboard`, `mouse` and `touch` // type `pointer` will be evaluated in `pointerMap` for IE Browser events this.inputEventMap = { 'keydown': 'keyboard', 'mousedown': 'mouse', 'mouseenter': 'mouse', 'touchstart': 'touch', 'pointerdown': 'pointer', 'MSPointerDown': 'pointer' }; // IE PointerDown events will be validated in `touch` or `mouse` // Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx this.iePointerMap = { 2: 'touch', 3: 'touch', 4: 'mouse' }; this.initializeEvents(); } /** * Initializes the interaction service, by registering all interaction events to the * body element. */ MdInteractionService.prototype.initializeEvents = function() { // IE browsers can also trigger pointer events, which also leads to an interaction. var pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null; this.bodyElement.on('keydown mousedown', this.onInputEvent.bind(this)); if ('ontouchstart' in document.documentElement) { this.bodyElement.on('touchstart', this.onBufferInputEvent.bind(this)); } if (pointerEvent) { this.bodyElement.on(pointerEvent, this.onInputEvent.bind(this)); } }; /** * Event listener for normal interaction events, which should be tracked. * @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent} */ MdInteractionService.prototype.onInputEvent = function(event) { if (this.isBuffering) { return; } var type = this.inputEventMap[event.type]; if (type === 'pointer') { type = this.iePointerMap[event.pointerType] || event.pointerType; } this.lastInteractionType = type; this.lastInteractionTime = this.$mdUtil.now(); }; /** * Event listener for interaction events which should be buffered (touch events). * @param event {TouchEvent} */ MdInteractionService.prototype.onBufferInputEvent = function(event) { this.$timeout.cancel(this.bufferTimeout); this.onInputEvent(event); this.isBuffering = true; // The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call // the `onInput` function multiple times. this.bufferTimeout = this.$timeout(function() { this.isBuffering = false; }.bind(this), 650, false); }; /** * @ngdoc method * @name $mdInteraction#getLastInteractionType * @description Retrieves the last interaction type triggered in body. * @returns {string|null} Last interaction type. */ MdInteractionService.prototype.getLastInteractionType = function() { return this.lastInteractionType; }; /** * @ngdoc method * @name $mdInteraction#isUserInvoked * @description Method to detect whether any interaction happened recently or not. * @param {number=} checkDelay Time to check for any interaction to have been triggered. * @returns {boolean} Whether there was any interaction or not. */ MdInteractionService.prototype.isUserInvoked = function(checkDelay) { var delay = angular.isNumber(checkDelay) ? checkDelay : 15; // Check for any interaction to be within the specified check time. return this.lastInteractionTime >= this.$mdUtil.now() - delay; }; })(); (function(){ "use strict"; angular.module('material.core') .provider('$$interimElement', InterimElementProvider); /* * @ngdoc service * @name $$interimElement * @module material.core * * @description * * Factory that contructs `$$interimElement.$service` services. * Used internally in material design for elements that appear on screen temporarily. * The service provides a promise-like API for interacting with the temporary * elements. * * ```js * app.service('$mdToast', function($$interimElement) { * var $mdToast = $$interimElement(toastDefaultOptions); * return $mdToast; * }); * ``` * @param {object=} defaultOptions Options used by default for the `show` method on the service. * * @returns {$$interimElement.$service} * */ function InterimElementProvider() { InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector", "$exceptionHandler"]; createInterimElementProvider.$get = InterimElementFactory; return createInterimElementProvider; /** * Returns a new provider which allows configuration of a new interimElement * service. Allows configuration of default options & methods for options, * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) */ function createInterimElementProvider(interimFactoryName) { factory.$inject = ["$$interimElement", "$injector"]; var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; var customMethods = {}; var providerConfig = { presets: {} }; var provider = { setDefaults: setDefaults, addPreset: addPreset, addMethod: addMethod, $get: factory }; /** * all interim elements will come with the 'build' preset */ provider.addPreset('build', { methods: ['controller', 'controllerAs', 'resolve', 'multiple', 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement'] }); return provider; /** * Save the configured defaults to be used when the factory is instantiated */ function setDefaults(definition) { providerConfig.optionsFactory = definition.options; providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS); return provider; } /** * Add a method to the factory that isn't specific to any interim element operations */ function addMethod(name, fn) { customMethods[name] = fn; return provider; } /** * Save the configured preset to be used when the factory is instantiated */ function addPreset(name, definition) { definition = definition || {}; definition.methods = definition.methods || []; definition.options = definition.options || function() { return {}; }; if (/^cancel|hide|show$/.test(name)) { throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!"); } if (definition.methods.indexOf('_options') > -1) { throw new Error("Method '_options' in " + interimFactoryName + " is reserved!"); } providerConfig.presets[name] = { methods: definition.methods.concat(EXPOSED_METHODS), optionsFactory: definition.options, argOption: definition.argOption }; return provider; } function addPresetMethod(presetName, methodName, method) { providerConfig.presets[presetName][methodName] = method; } /** * Create a factory that has the given methods & defaults implementing interimElement */ /* @ngInject */ function factory($$interimElement, $injector) { var defaultMethods; var defaultOptions; var interimElementService = $$interimElement(); /* * publicService is what the developer will be using. * It has methods hide(), cancel(), show(), build(), and any other * presets which were set during the config phase. */ var publicService = { hide: interimElementService.hide, cancel: interimElementService.cancel, show: showInterimElement, // Special internal method to destroy an interim element without animations // used when navigation changes causes a $scope.$destroy() action destroy : destroyInterimElement }; defaultMethods = providerConfig.methods || []; // This must be invoked after the publicService is initialized defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); // Copy over the simple custom methods angular.forEach(customMethods, function(fn, name) { publicService[name] = fn; }); angular.forEach(providerConfig.presets, function(definition, name) { var presetDefaults = invokeFactory(definition.optionsFactory, {}); var presetMethods = (definition.methods || []).concat(defaultMethods); // Every interimElement built with a preset has a field called `$type`, // which matches the name of the preset. // Eg in preset 'confirm', options.$type === 'confirm' angular.extend(presetDefaults, { $type: name }); // This creates a preset class which has setter methods for every // method given in the `.addPreset()` function, as well as every // method given in the `.setDefaults()` function. // // @example // .setDefaults({ // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'], // options: dialogDefaultOptions // }) // .addPreset('alert', { // methods: ['title', 'ok'], // options: alertDialogOptions // }) // // Set values will be passed to the options when interimElement.show() is called. function Preset(opts) { this._options = angular.extend({}, presetDefaults, opts); } angular.forEach(presetMethods, function(name) { Preset.prototype[name] = function(value) { this._options[name] = value; return this; }; }); // Create shortcut method for one-linear methods if (definition.argOption) { var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); publicService[methodName] = function(arg) { var config = publicService[name](arg); return publicService.show(config); }; } // eg $mdDialog.alert() will return a new alert preset publicService[name] = function(arg) { // If argOption is supplied, eg `argOption: 'content'`, then we assume // if the argument is not an options object then it is the `argOption` option. // // @example `$mdToast.simple('hello')` // sets options.content to hello // // because argOption === 'content' if (arguments.length && definition.argOption && !angular.isObject(arg) && !angular.isArray(arg)) { return (new Preset())[definition.argOption](arg); } else { return new Preset(arg); } }; }); return publicService; /** * */ function showInterimElement(opts) { // opts is either a preset which stores its options on an _options field, // or just an object made up of options opts = opts || { }; if (opts._options) opts = opts._options; return interimElementService.show( angular.extend({}, defaultOptions, opts) ); } /** * Special method to hide and destroy an interimElement WITHOUT * any 'leave` or hide animations ( an immediate force hide/remove ) * * NOTE: This calls the onRemove() subclass method for each component... * which must have code to respond to `options.$destroy == true` */ function destroyInterimElement(opts) { return interimElementService.destroy(opts); } /** * Helper to call $injector.invoke with a local of the factory name for * this provider. * If an $mdDialog is providing options for a dialog and tries to inject * $mdDialog, a circular dependency error will happen. * We get around that by manually injecting $mdDialog as a local. */ function invokeFactory(factory, defaultVal) { var locals = {}; locals[interimFactoryName] = publicService; return $injector.invoke(factory || function() { return defaultVal; }, {}, locals); } } } /* @ngInject */ function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, $mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) { return function createInterimElementService() { var SHOW_CANCELLED = false; /* * @ngdoc service * @name $$interimElement.$service * * @description * A service used to control inserting and removing an element into the DOM. * */ var service; var showPromises = []; // Promises for the interim's which are currently opening. var hidePromises = []; // Promises for the interim's which are currently hiding. var showingInterims = []; // Interim elements which are currently showing up. // Publish instance $$interimElement service; // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect return service = { show: show, hide: waitForInterim(hide), cancel: waitForInterim(cancel), destroy : destroy, $injector_: $injector }; /* * @ngdoc method * @name $$interimElement.$service#show * @kind function * * @description * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected * with hide or cancel, respectively. To external cancel/hide, developers should use the * * @param {*} options is hashMap of settings * @returns a Promise * */ function show(options) { options = options || {}; var interimElement = new InterimElement(options || {}); // When an interim element is currently showing, we have to cancel it. // Just hiding it, will resolve the InterimElement's promise, the promise should be // rejected instead. var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises); if (!options.multiple) { // Wait for all opening interim's to finish their transition. hideAction = hideAction.then(function() { // Wait for all closing and showing interim's to be completely closed. var promiseArray = hidePromises.concat(showingInterims.map(service.cancel)); return $q.all(promiseArray); }); } var showAction = hideAction.then(function() { return interimElement .show() .catch(function(reason) { return reason; }) .finally(function() { showPromises.splice(showPromises.indexOf(showAction), 1); showingInterims.push(interimElement); }); }); showPromises.push(showAction); // In AngularJS 1.6+, exceptions inside promises will cause a rejection. We need to handle // the rejection and only log it if it's an error. interimElement.deferred.promise.catch(function(fault) { if (fault instanceof Error) { $exceptionHandler(fault); } return fault; }); // Return a promise that will be resolved when the interim // element is hidden or cancelled... return interimElement.deferred.promise; } /* * @ngdoc method * @name $$interimElement.$service#hide * @kind function * * @description * Removes the `$interimElement` from the DOM and resolves the promise returned from `show` * * @param {*} resolveParam Data to resolve the promise with * @returns a Promise that will be resolved after the element has been removed. * */ function hide(reason, options) { options = options || {}; if (options.closeAll) { // We have to make a shallow copy of the array, because otherwise the map will break. return $q.all(showingInterims.slice().reverse().map(closeElement)); } else if (options.closeTo !== undefined) { return $q.all(showingInterims.slice(options.closeTo).map(closeElement)); } // Hide the latest showing interim element. return closeElement(showingInterims[showingInterims.length - 1]); function closeElement(interim) { var hideAction = interim .remove(reason, false, options || { }) .catch(function(reason) { return reason; }) .finally(function() { hidePromises.splice(hidePromises.indexOf(hideAction), 1); }); showingInterims.splice(showingInterims.indexOf(interim), 1); hidePromises.push(hideAction); return interim.deferred.promise; } } /* * @ngdoc method * @name $$interimElement.$service#cancel * @kind function * * @description * Removes the `$interimElement` from the DOM and rejects the promise returned from `show` * * @param {*} reason Data to reject the promise with * @returns Promise that will be resolved after the element has been removed. * */ function cancel(reason, options) { var interim = showingInterims.pop(); if (!interim) { return $q.when(reason); } var cancelAction = interim .remove(reason, true, options || {}) .catch(function(reason) { return reason; }) .finally(function() { hidePromises.splice(hidePromises.indexOf(cancelAction), 1); }); hidePromises.push(cancelAction); // Since AngularJS 1.6.7, promises will be logged to $exceptionHandler when the promise // is not handling the rejection. We create a pseudo catch handler, which will prevent the // promise from being logged to the $exceptionHandler. return interim.deferred.promise.catch(angular.noop); } /** * Creates a function to wait for at least one interim element to be available. * @param callbackFn Function to be used as callback * @returns {Function} */ function waitForInterim(callbackFn) { return function() { var fnArguments = arguments; if (!showingInterims.length) { // When there are still interim's opening, then wait for the first interim element to // finish its open animation. if (showPromises.length) { return showPromises[0].finally(function () { return callbackFn.apply(service, fnArguments); }); } return $q.when("No interim elements currently showing up."); } return callbackFn.apply(service, fnArguments); }; } /* * Special method to quick-remove the interim element without animations * Note: interim elements are in "interim containers" */ function destroy(targetEl) { var interim = !targetEl ? showingInterims.shift() : null; var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode; if (parentEl) { // Try to find the interim in the stack which corresponds to the supplied DOM element. var filtered = showingInterims.filter(function(entry) { return entry.options.element[0] === parentEl; }); // Note: This function might be called when the element already has been removed, // in which case we won't find any matches. if (filtered.length) { interim = filtered[0]; showingInterims.splice(showingInterims.indexOf(interim), 1); } } return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) : $q.when(SHOW_CANCELLED); } /* * Internal Interim Element Object * Used internally to manage the DOM element and related data */ function InterimElement(options) { var self, element, showAction = $q.when(true); options = configureScopeAndTransitions(options); return self = { options : options, deferred: $q.defer(), show : createAndTransitionIn, remove : transitionOutAndRemove }; /** * Compile, link, and show this interim element * Use optional autoHided and transition-in effects */ function createAndTransitionIn() { return $q(function(resolve, reject) { // Trigger onCompiling callback before the compilation starts. // This is useful, when modifying options, which can be influenced by developers. options.onCompiling && options.onCompiling(options); compileElement(options) .then(function( compiledData ) { element = linkElement( compiledData, options ); // Expose the cleanup function from the compiler. options.cleanupElement = compiledData.cleanup; showAction = showElement(element, options, compiledData.controller) .then(resolve, rejectAll); }).catch(rejectAll); function rejectAll(fault) { // Force the '$md.show()' promise to reject self.deferred.reject(fault); // Continue rejection propagation reject(fault); } }); } /** * After the show process has finished/rejected: * - announce 'removing', * - perform the transition-out, and * - perform optional clean up scope. */ function transitionOutAndRemove(response, isCancelled, opts) { // abort if the show() and compile failed if ( !element ) return $q.when(false); options = angular.extend(options || {}, opts || {}); options.cancelAutoHide && options.cancelAutoHide(); options.element.triggerHandler('$mdInterimElementRemove'); if ( options.$destroy === true ) { return hideElement(options.element, options).then(function(){ (isCancelled && rejectAll(response)) || resolveAll(response); }); } else { $q.when(showAction).finally(function() { hideElement(options.element, options).then(function() { isCancelled ? rejectAll(response) : resolveAll(response); }, rejectAll); }); return self.deferred.promise; } /** * The `show()` returns a promise that will be resolved when the interim * element is hidden or cancelled... */ function resolveAll(response) { self.deferred.resolve(response); } /** * Force the '$md.show()' promise to reject */ function rejectAll(fault) { self.deferred.reject(fault); } } /** * Prepare optional isolated scope and prepare $animate with default enter and leave * transitions for the new element instance. */ function configureScopeAndTransitions(options) { options = options || { }; if ( options.template ) { options.template = $mdUtil.processTemplate(options.template); } return angular.extend({ preserveScope: false, cancelAutoHide : angular.noop, scope: options.scope || $rootScope.$new(options.isolateScope), /** * Default usage to enable $animate to transition-in; can be easily overridden via 'options' */ onShow: function transitionIn(scope, element, options) { return $animate.enter(element, options.parent); }, /** * Default usage to enable $animate to transition-out; can be easily overridden via 'options' */ onRemove: function transitionOut(scope, element) { // Element could be undefined if a new element is shown before // the old one finishes compiling. return element && $animate.leave(element) || $q.when(); } }, options ); } /** * Compile an element with a templateUrl, controller, and locals */ function compileElement(options) { var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; return compiled || $q(function (resolve) { resolve({ locals: {}, link: function () { return options.element; } }); }); } /** * Link an element with compiled configuration */ function linkElement(compileData, options){ angular.extend(compileData.locals, options); var element = compileData.link(options.scope); // Search for parent at insertion time, if not specified options.element = element; options.parent = findParent(element, options); if (options.themable) $mdTheming(element); return element; } /** * Search for parent at insertion time, if not specified */ function findParent(element, options) { var parent = options.parent; // Search for parent at insertion time, if not specified if (angular.isFunction(parent)) { parent = parent(options.scope, element, options); } else if (angular.isString(parent)) { parent = angular.element($document[0].querySelector(parent)); } else { parent = angular.element(parent); } // If parent querySelector/getter function fails, or it's just null, // find a default. if (!(parent || {}).length) { var el; if ($rootElement[0] && $rootElement[0].querySelector) { el = $rootElement[0].querySelector(':not(svg) > body'); } if (!el) el = $rootElement[0]; if (el.nodeName == '#comment') { el = $document[0].body; } return angular.element(el); } return parent; } /** * If auto-hide is enabled, start timer and prepare cancel function */ function startAutoHide() { var autoHideTimer, cancelAutoHide = angular.noop; if (options.hideDelay) { autoHideTimer = $timeout(service.hide, options.hideDelay) ; cancelAutoHide = function() { $timeout.cancel(autoHideTimer); }; } // Cache for subsequent use options.cancelAutoHide = function() { cancelAutoHide(); options.cancelAutoHide = undefined; }; } /** * Show the element ( with transitions), notify complete and start * optional auto-Hide */ function showElement(element, options, controller) { // Trigger onShowing callback before the `show()` starts var notifyShowing = options.onShowing || angular.noop; // Trigger onComplete callback when the `show()` finishes var notifyComplete = options.onComplete || angular.noop; // Necessary for consistency between AngularJS 1.5 and 1.6. try { notifyShowing(options.scope, element, options, controller); } catch (e) { return $q.reject(e); } return $q(function (resolve, reject) { try { // Start transitionIn $q.when(options.onShow(options.scope, element, options, controller)) .then(function () { notifyComplete(options.scope, element, options); startAutoHide(); resolve(element); }, reject); } catch (e) { reject(e.message); } }); } function hideElement(element, options) { var announceRemoving = options.onRemoving || angular.noop; return $q(function (resolve, reject) { try { // Start transitionIn var action = $q.when( options.onRemove(options.scope, element, options) || true ); // Trigger callback *before* the remove operation starts announceRemoving(element, action); if (options.$destroy) { // For $destroy, onRemove should be synchronous resolve(element); if (!options.preserveScope && options.scope ) { // scope destroy should still be be done after the current digest is done action.then( function() { options.scope.$destroy(); }); } } else { // Wait until transition-out is done action.then(function () { if (!options.preserveScope && options.scope ) { options.scope.$destroy(); } resolve(element); }, reject); } } catch (e) { reject(e.message); } }); } } }; } } })(); (function(){ "use strict"; (function() { 'use strict'; var $mdUtil, $interpolate, $log; var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g; var WHITESPACE = /\s+/g; var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ]; var LAYOUT_OPTIONS = ['row', 'column']; var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ]; var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ]; var config = { /** * Enable directive attribute-to-class conversions * Developers can use `` to quickly * disable the Layout directives and prohibit the injection of Layout classNames */ enabled: true, /** * List of mediaQuery breakpoints and associated suffixes * * [ * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } * ] */ breakpoints: [] }; registerLayoutAPI( angular.module('material.core.layout', ['ng']) ); /** * registerLayoutAPI() * * The original AngularJS Material Layout solution used attribute selectors and CSS. * * ```html *
My Content
* ``` * * ```css * [layout] { * box-sizing: border-box; * display:flex; * } * [layout=column] { * flex-direction : column * } * ``` * * Use of attribute selectors creates significant performance impacts in some * browsers... mainly IE. * * This module registers directives that allow the same layout attributes to be * interpreted and converted to class selectors. The directive will add equivalent classes to each element that * contains a Layout directive. * * ```html *
My Content
*``` * * ```css * .layout { * box-sizing: border-box; * display:flex; * } * .layout-column { * flex-direction : column * } * ``` */ function registerLayoutAPI(module){ var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ]; var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ]; var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ]; // Build directive registration functions for the standard Layout API... for all breakpoints. angular.forEach(BREAKPOINTS, function(mqb) { // Attribute directives with expected, observable value(s) angular.forEach( API_WITH_VALUES, function(name){ var fullName = mqb ? name + "-" + mqb : name; module.directive( directiveNormalize(fullName), attributeWithObserve(fullName)); }); // Attribute directives with no expected value(s) angular.forEach( API_NO_VALUES, function(name){ var fullName = mqb ? name + "-" + mqb : name; module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName)); }); }); // Register other, special directive functions for the Layout features: module .provider('$$mdLayout' , function() { // Publish internal service for Layouts return { $get : angular.noop, validateAttributeValue : validateAttributeValue, validateAttributeUsage : validateAttributeUsage, /** * Easy way to disable/enable the Layout API. * When disabled, this stops all attribute-to-classname generations */ disableLayouts : function(isDisabled) { config.enabled = (isDisabled !== true); } }; }) .directive('mdLayoutCss' , disableLayoutDirective ) .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap')) .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap')) .directive('layoutFill' , attributeWithoutValue('layout-fill')) // !! Deprecated attributes: use the `-lt` (aka less-than) notations .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true)) .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true)) .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true)) .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true)) .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md')) .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg')) .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md')) .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg')) .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md')) .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg')) .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')) // Determine if .config( detectDisabledLayouts ); /** * Converts snake_case to camelCase. * Also there is special case for Moz prefix starting with upper case letter. * @param name Name to normalize */ function directiveNormalize(name) { return name .replace(PREFIX_REGEXP, '') .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { return offset ? letter.toUpperCase() : letter; }); } } /** * Detect if any of the HTML tags has a [md-layouts-disabled] attribute; * If yes, then immediately disable all layout API features * * Note: this attribute should be specified on either the HTML or BODY tags */ /** * @ngInject */ function detectDisabledLayouts() { var isDisabled = !!document.querySelector('[md-layouts-disabled]'); config.enabled = !isDisabled; } /** * Special directive that will disable ALL Layout conversions of layout * attribute(s) to classname(s). * * * * * * ... * * * Note: Using md-layout-css directive requires the developer to load the Material * Layout Attribute stylesheet (which only uses attribute selectors): * * `angular-material.layout.css` * * Another option is to use the LayoutProvider to configure and disable the attribute * conversions; this would obviate the use of the `md-layout-css` directive * */ function disableLayoutDirective() { // Return a 1x-only, first-match attribute directive config.enabled = false; return { restrict : 'A', priority : '900' }; } /** * Tail-hook ngCloak to delay the uncloaking while Layout transformers * finish processing. Eliminates flicker with Material.Layouts */ function buildCloakInterceptor(className) { return [ '$timeout', function($timeout){ return { restrict : 'A', priority : -10, // run after normal ng-cloak compile : function( element ) { if (!config.enabled) return angular.noop; // Re-add the cloak element.addClass(className); return function( scope, element ) { // Wait while layout injectors configure, then uncloak // NOTE: $rAF does not delay enough... and this is a 1x-only event, // $timeout is acceptable. $timeout( function(){ element.removeClass(className); }, 10, false); }; } }; }]; } // ********************************************************************************* // // These functions create registration functions for AngularJS Material Layout attribute directives // This provides easy translation to switch AngularJS Material attribute selectors to // CLASS selectors and directives; which has huge performance implications // for IE Browsers // // ********************************************************************************* /** * Creates a directive registration function where a possible dynamic attribute * value will be observed/watched. * @param {string} className attribute name; eg `layout-gt-md` with value ="row" */ function attributeWithObserve(className) { return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { $mdUtil = _$mdUtil_; $interpolate = _$interpolate_; $log = _$log_; return { restrict: 'A', compile: function(element, attr) { var linkFn; if (config.enabled) { // immediately replace static (non-interpolated) invalid values... validateAttributeUsage(className, attr, element, $log); validateAttributeValue( className, getNormalizedAttrValue(className, attr, ""), buildUpdateFn(element, className, attr) ); linkFn = translateWithValueToCssClass; } // Use for postLink to account for transforms after ng-transclude. return linkFn || angular.noop; } }; }]; /** * Add as transformed class selector(s), then * remove the deprecated attribute selector */ function translateWithValueToCssClass(scope, element, attrs) { var updateFn = updateClassWithValue(element, className, attrs); var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); updateFn(getNormalizedAttrValue(className, attrs, "")); scope.$on("$destroy", function() { unwatch(); }); } } /** * Creates a registration function for AngularJS Material Layout attribute directive. * This is a `simple` transpose of attribute usage to class usage; where we ignore * any attribute value */ function attributeWithoutValue(className) { return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { $mdUtil = _$mdUtil_; $interpolate = _$interpolate_; $log = _$log_; return { restrict: 'A', compile: function(element, attr) { var linkFn; if (config.enabled) { // immediately replace static (non-interpolated) invalid values... validateAttributeValue( className, getNormalizedAttrValue(className, attr, ""), buildUpdateFn(element, className, attr) ); translateToCssClass(null, element); // Use for postLink to account for transforms after ng-transclude. linkFn = translateToCssClass; } return linkFn || angular.noop; } }; }]; /** * Add as transformed class selector, then * remove the deprecated attribute selector */ function translateToCssClass(scope, element) { element.addClass(className); } } /** * After link-phase, do NOT remove deprecated layout attribute selector. * Instead watch the attribute so interpolated data-bindings to layout * selectors will continue to be supported. * * $observe() the className and update with new class (after removing the last one) * * e.g. `layout="{{layoutDemo.direction}}"` will update... * * NOTE: The value must match one of the specified styles in the CSS. * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined. * */ function updateClassWithValue(element, className) { var lastClass; return function updateClassFn(newValue) { var value = validateAttributeValue(className, newValue || ""); if ( angular.isDefined(value) ) { if (lastClass) element.removeClass(lastClass); lastClass = !value ? className : className + "-" + value.trim().replace(WHITESPACE, "-"); element.addClass(lastClass); } }; } /** * Provide console warning that this layout attribute has been deprecated * */ function warnAttrNotSupported(className) { var parts = className.split("-"); return ["$log", function($log) { $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-` variant."); return angular.noop; }]; } /** * Centralize warnings for known flexbox issues (especially IE-related issues) */ function validateAttributeUsage(className, attr, element, $log){ var message, usage, url; var nodeName = element[0].nodeName.toLowerCase(); switch(className.replace(SUFFIXES,"")) { case "flex": if ((nodeName == "md-button") || (nodeName == "fieldset")){ // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers // Use
wrapper inside (preferred) or outside usage = "<" + nodeName + " " + className + ">"; url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers"; message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details."; $log.warn( $mdUtil.supplant(message, [usage, url]) ); } } } /** * For the Layout attribute value, validate or replace with default * fallback value */ function validateAttributeValue(className, value, updateFn) { var origValue; if (!needsInterpolation(value)) { switch (className.replace(SUFFIXES,"")) { case 'layout' : if ( !findIn(value, LAYOUT_OPTIONS) ) { value = LAYOUT_OPTIONS[0]; // 'row'; } break; case 'flex' : if (!findIn(value, FLEX_OPTIONS)) { if (isNaN(value)) { value = ''; } } break; case 'flex-offset' : case 'flex-order' : if (!value || isNaN(+value)) { value = '0'; } break; case 'layout-align' : var axis = extractAlignAxis(value); value = $mdUtil.supplant("{main}-{cross}",axis); break; case 'layout-padding' : case 'layout-margin' : case 'layout-fill' : case 'layout-wrap' : case 'layout-nowrap' : case 'layout-nowrap' : value = ''; break; } if (value != origValue) { (updateFn || angular.noop)(value); } } return value ? value.trim() : ""; } /** * Replace current attribute value with fallback value */ function buildUpdateFn(element, className, attrs) { return function updateAttrValue(fallback) { if (!needsInterpolation(fallback)) { // Do not modify the element's attribute value; so // uses '' will not // be affected. Just update the attrs value. attrs[attrs.$normalize(className)] = fallback; } }; } /** * See if the original value has interpolation symbols: * e.g. flex-gt-md="{{triggerPoint}}" */ function needsInterpolation(value) { return (value || "").indexOf($interpolate.startSymbol()) > -1; } function getNormalizedAttrValue(className, attrs, defaultVal) { var normalizedAttr = attrs.$normalize(className); return attrs[normalizedAttr] ? attrs[normalizedAttr].trim().replace(WHITESPACE, "-") : defaultVal || null; } function findIn(item, list, replaceWith) { item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item; var found = false; if (item) { list.forEach(function(it) { it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it; found = found || (it === item); }); } return found; } function extractAlignAxis(attrValue) { var axis = { main : "start", cross: "stretch" }, values; attrValue = (attrValue || ""); if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) { // For missing main-axis values attrValue = "none" + attrValue; } values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-"); if ( values.length && (values[0] === "space") ) { // for main-axis values of "space-around" or "space-between" values = [ values[0]+"-"+values[1],values[2] ]; } if ( values.length > 0 ) axis.main = values[0] || axis.main; if ( values.length > 1 ) axis.cross = values[1] || axis.cross; if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start"; if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch"; return axis; } })(); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.liveannouncer * @description * AngularJS Material Live Announcer to provide accessibility for Voice Readers. */ MdLiveAnnouncer.$inject = ["$timeout"]; angular .module('material.core') .service('$mdLiveAnnouncer', MdLiveAnnouncer); /** * @ngdoc service * @name $mdLiveAnnouncer * @module material.core.liveannouncer * * @description * * Service to announce messages to supported screenreaders. * * > The `$mdLiveAnnouncer` service is internally used for components to provide proper accessibility. * * * module.controller('AppCtrl', function($mdLiveAnnouncer) { * // Basic announcement (Polite Mode) * $mdLiveAnnouncer.announce('Hey Google'); * * // Custom announcement (Assertive Mode) * $mdLiveAnnouncer.announce('Hey Google', 'assertive'); * }); * * */ function MdLiveAnnouncer($timeout) { /** @private @const @type {!angular.$timeout} */ this._$timeout = $timeout; /** @private @const @type {!HTMLElement} */ this._liveElement = this._createLiveElement(); /** @private @const @type {!number} */ this._announceTimeout = 100; } /** * @ngdoc method * @name $mdLiveAnnouncer#announce * @description Announces messages to supported screenreaders. * @param {string} message Message to be announced to the screenreader * @param {'off'|'polite'|'assertive'} politeness The politeness of the announcer element. */ MdLiveAnnouncer.prototype.announce = function(message, politeness) { if (!politeness) { politeness = 'polite'; } var self = this; self._liveElement.textContent = ''; self._liveElement.setAttribute('aria-live', politeness); // This 100ms timeout is necessary for some browser + screen-reader combinations: // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a // second time without clearing and then using a non-zero delay. // (using JAWS 17 at time of this writing). self._$timeout(function() { self._liveElement.textContent = message; }, self._announceTimeout, false); }; /** * Creates a live announcer element, which listens for DOM changes and announces them * to the screenreaders. * @returns {!HTMLElement} * @private */ MdLiveAnnouncer.prototype._createLiveElement = function() { var liveEl = document.createElement('div'); liveEl.classList.add('md-visually-hidden'); liveEl.setAttribute('role', 'status'); liveEl.setAttribute('aria-atomic', 'true'); liveEl.setAttribute('aria-live', 'polite'); document.body.appendChild(liveEl); return liveEl; }; })(); (function(){ "use strict"; /** * @ngdoc service * @name $$mdMeta * @module material.core.meta * * @description * * A provider and a service that simplifies meta tags access * * Note: This is intended only for use with dynamic meta tags such as browser color and title. * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`) * will not work since `$$mdMeta` adds the tags after the page has already been loaded. * * ```js * app.config(function($$mdMetaProvider) { * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content'); * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content' * * removeMeta(); * }); * * app.controller('myController', function($$mdMeta) { * var removeMeta = $$mdMeta.setMeta('meta-name', 'content'); * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content' * * removeMeta(); * }); * ``` * * @returns {$$mdMeta.$service} * */ angular.module('material.core.meta', []) .provider('$$mdMeta', function () { var head = angular.element(document.head); var metaElements = {}; /** * Checks if the requested element was written manually and maps it * * @param {string} name meta tag 'name' attribute value * @returns {boolean} returns true if there is an element with the requested name */ function mapExistingElement(name) { if (metaElements[name]) { return true; } var element = document.getElementsByName(name)[0]; if (!element) { return false; } metaElements[name] = angular.element(element); return true; } /** * @ngdoc method * @name $$mdMeta#setMeta * * @description * Creates meta element with the 'name' and 'content' attributes, * if the meta tag is already created than we replace the 'content' value * * @param {string} name meta tag 'name' attribute value * @param {string} content meta tag 'content' attribute value * @returns {function} remove function * */ function setMeta(name, content) { mapExistingElement(name); if (!metaElements[name]) { var newMeta = angular.element(''); head.append(newMeta); metaElements[name] = newMeta; } else { metaElements[name].attr('content', content); } return function () { metaElements[name].attr('content', ''); metaElements[name].remove(); delete metaElements[name]; }; } /** * @ngdoc method * @name $$mdMeta#getMeta * * @description * Gets the 'content' attribute value of the wanted meta element * * @param {string} name meta tag 'name' attribute value * @returns {string} content attribute value */ function getMeta(name) { if (!mapExistingElement(name)) { throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); } return metaElements[name].attr('content'); } var module = { setMeta: setMeta, getMeta: getMeta }; return angular.extend({}, module, { $get: function () { return module; } }); }); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.componentRegistry * * @description * A component instance registration service. * Note: currently this as a private service in the SideNav component. */ ComponentRegistry.$inject = ["$log", "$q"]; angular.module('material.core') .factory('$mdComponentRegistry', ComponentRegistry); /* * @private * @ngdoc factory * @name ComponentRegistry * @module material.core.componentRegistry * */ function ComponentRegistry($log, $q) { var self; var instances = [ ]; var pendings = { }; return self = { /** * Used to print an error when an instance for a handle isn't found. */ notFoundError: function(handle, msgContext) { $log.error( (msgContext || "") + 'No instance found for handle', handle); }, /** * Return all registered instances as an array. */ getInstances: function() { return instances; }, /** * Get a registered instance. * @param handle the String handle to look up for a registered instance. */ get: function(handle) { if ( !isValidID(handle) ) return null; var i, j, instance; for(i = 0, j = instances.length; i < j; i++) { instance = instances[i]; if(instance.$$mdHandle === handle) { return instance; } } return null; }, /** * Register an instance. * @param instance the instance to register * @param handle the handle to identify the instance under. */ register: function(instance, handle) { if ( !handle ) return angular.noop; instance.$$mdHandle = handle; instances.push(instance); resolveWhen(); return deregister; /** * Remove registration for an instance */ function deregister() { var index = instances.indexOf(instance); if (index !== -1) { instances.splice(index, 1); } } /** * Resolve any pending promises for this instance */ function resolveWhen() { var dfd = pendings[handle]; if ( dfd ) { dfd.forEach(function (promise) { promise.resolve(instance); }); delete pendings[handle]; } } }, /** * Async accessor to registered component instance * If not available then a promise is created to notify * all listeners when the instance is registered. */ when : function(handle) { if ( isValidID(handle) ) { var deferred = $q.defer(); var instance = self.get(handle); if ( instance ) { deferred.resolve( instance ); } else { if (pendings[handle] === undefined) { pendings[handle] = []; } pendings[handle].push(deferred); } return deferred.promise; } return $q.reject("Invalid `md-component-id` value."); } }; function isValidID(handle){ return handle && (handle !== ""); } } })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdButtonInkRipple * @module material.core * * @description * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the default ripple configuration */ MdButtonInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdButtonInkRipple', MdButtonInkRipple); function MdButtonInkRipple($mdInkRipple) { return { attach: function attachRipple(scope, element, options) { options = angular.extend(optionsForElement(element), options); return $mdInkRipple.attach(scope, element, options); } }; function optionsForElement(element) { if (element.hasClass('md-icon-button')) { return { isMenuItem: element.hasClass('md-menu-item'), fitRipple: true, center: true }; } else { return { isMenuItem: element.hasClass('md-menu-item'), dimBackground: true }; } } } })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdCheckboxInkRipple * @module material.core * * @description * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ MdCheckboxInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); function MdCheckboxInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { return $mdInkRipple.attach(scope, element, angular.extend({ center: true, dimBackground: false, fitRipple: true }, options)); } } })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdListInkRipple * @module material.core * * @description * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ MdListInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdListInkRipple', MdListInkRipple); function MdListInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { return $mdInkRipple.attach(scope, element, angular.extend({ center: false, dimBackground: true, outline: false, rippleSize: 'full' }, options)); } } })(); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.core.ripple * @description * Ripple */ InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"]; InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; angular.module('material.core') .provider('$mdInkRipple', InkRippleProvider) .directive('mdInkRipple', InkRippleDirective) .directive('mdNoInk', attrNoDirective) .directive('mdNoBar', attrNoDirective) .directive('mdNoStretch', attrNoDirective); var DURATION = 450; /** * @ngdoc directive * @name mdInkRipple * @module material.core.ripple * * @description * The `md-ink-ripple` directive allows you to specify the ripple color or if a ripple is allowed. * * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple * * @usage * ### String values * * * Ripples in red * * * * Not rippling * * * * ### Interpolated values * * * Ripples with the return value of 'randomColor' function * * * * Ripples if 'canRipple' function return value is not 'false' or '0' * * */ function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { return { controller: angular.noop, link: function (scope, element, attr) { attr.hasOwnProperty('mdInkRippleCheckbox') ? $mdCheckboxInkRipple.attach(scope, element) : $mdButtonInkRipple.attach(scope, element); } }; } /** * @ngdoc service * @name $mdInkRipple * @module material.core.ripple * * @description * `$mdInkRipple` is a service for adding ripples to any element * * @usage * * app.factory('$myElementInkRipple', function($mdInkRipple) { * return { * attach: function (scope, element, options) { * return $mdInkRipple.attach(scope, element, angular.extend({ * center: false, * dimBackground: true * }, options)); * } * }; * }); * * app.controller('myController', function ($scope, $element, $myElementInkRipple) { * $scope.onClick = function (ev) { * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true }); * } * }); * * * ### Disabling ripples globally * If you want to disable ink ripples globally, for all components, you can call the * `disableInkRipple` method in your app's config. * * * app.config(function ($mdInkRippleProvider) { * $mdInkRippleProvider.disableInkRipple(); * }); */ function InkRippleProvider () { var isDisabledGlobally = false; return { disableInkRipple: disableInkRipple, $get: ["$injector", function($injector) { return { attach: attach }; /** * @ngdoc method * @name $mdInkRipple#attach * * @description * Attaching given scope, element and options to inkRipple controller * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration * * `center` - Whether the ripple should start from the center of the container element * * `dimBackground` - Whether the background should be dimmed with the ripple color * * `colorElement` - The element the ripple should take its color from, defined by css property `color` * * `fitRipple` - Whether the ripple should fill the element */ function attach (scope, element, options) { if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop; return $injector.instantiate(InkRippleCtrl, { $scope: scope, $element: element, rippleOptions: options }); } }] }; /** * @ngdoc method * @name $mdInkRipple#disableInkRipple * * @description * A config-time method that, when called, disables ripples globally. */ function disableInkRipple () { isDisabledGlobally = true; } } /** * Controller used by the ripple service in order to apply ripples * @ngInject */ function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) { this.$window = $window; this.$timeout = $timeout; this.$mdUtil = $mdUtil; this.$mdColorUtil = $mdColorUtil; this.$scope = $scope; this.$element = $element; this.options = rippleOptions; this.mousedown = false; this.ripples = []; this.timeout = null; // Stores a reference to the most-recent ripple timeout this.lastRipple = null; $mdUtil.valueOnUse(this, 'container', this.createContainer); this.$element.addClass('md-ink-ripple'); // attach method for unit tests ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple); ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color); this.bindEvents(); } /** * Either remove or unlock any remaining ripples when the user mouses off of the element (either by * mouseup or mouseleave event) */ function autoCleanup (self, cleanupFn) { if ( self.mousedown || self.lastRipple ) { self.mousedown = false; self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false); } } /** * Returns the color that the ripple should be (either based on CSS or hard-coded) * @returns {string} */ InkRippleCtrl.prototype.color = function (value) { var self = this; // If assigning a color value, apply it to background and the ripple color if (angular.isDefined(value)) { self._color = self._parseColor(value); } // If color lookup, use assigned, defined, or inherited return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() ); /** * Finds the color element and returns its text color for use as default ripple color * @returns {string} */ function getElementColor () { var items = self.options && self.options.colorElement ? self.options.colorElement : []; var elem = items.length ? items[ 0 ] : self.$element[ 0 ]; return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)'; } }; /** * Updating the ripple colors based on the current inkRipple value * or the element's computed style color */ InkRippleCtrl.prototype.calculateColor = function () { return this.color(); }; /** * Takes a string color and converts it to RGBA format * @param color {string} * @param [multiplier] {int} * @returns {string} */ InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { multiplier = multiplier || 1; var colorUtil = this.$mdColorUtil; if (!color) return; if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')'); if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color); if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color); }; /** * Binds events to the root element for */ InkRippleCtrl.prototype.bindEvents = function () { this.$element.on('mousedown', angular.bind(this, this.handleMousedown)); this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup)); this.$element.on('mouseleave', angular.bind(this, this.handleMouseup)); this.$element.on('touchmove', angular.bind(this, this.handleTouchmove)); }; /** * Create a new ripple on every mousedown event from the root element * @param event {MouseEvent} */ InkRippleCtrl.prototype.handleMousedown = function (event) { if ( this.mousedown ) return; // When jQuery is loaded, we have to get the original event if (event.hasOwnProperty('originalEvent')) event = event.originalEvent; this.mousedown = true; if (this.options.center) { this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2); } else { // We need to calculate the relative coordinates if the target is a sublayer of the ripple element if (event.srcElement !== this.$element[0]) { var layerRect = this.$element[0].getBoundingClientRect(); var layerX = event.clientX - layerRect.left; var layerY = event.clientY - layerRect.top; this.createRipple(layerX, layerY); } else { this.createRipple(event.offsetX, event.offsetY); } } }; /** * Either remove or unlock any remaining ripples when the user mouses off of the element (either by * mouseup, touchend or mouseleave event) */ InkRippleCtrl.prototype.handleMouseup = function () { autoCleanup(this, this.clearRipples); }; /** * Either remove or unlock any remaining ripples when the user mouses off of the element (by * touchmove) */ InkRippleCtrl.prototype.handleTouchmove = function () { autoCleanup(this, this.deleteRipples); }; /** * Cycles through all ripples and attempts to remove them. */ InkRippleCtrl.prototype.deleteRipples = function () { for (var i = 0; i < this.ripples.length; i++) { this.ripples[ i ].remove(); } }; /** * Cycles through all ripples and attempts to remove them with fade. * Depending on logic within `fadeInComplete`, some removals will be postponed. */ InkRippleCtrl.prototype.clearRipples = function () { for (var i = 0; i < this.ripples.length; i++) { this.fadeInComplete(this.ripples[ i ]); } }; /** * Creates the ripple container element * @returns {*} */ InkRippleCtrl.prototype.createContainer = function () { var container = angular.element('
'); this.$element.append(container); return container; }; InkRippleCtrl.prototype.clearTimeout = function () { if (this.timeout) { this.$timeout.cancel(this.timeout); this.timeout = null; } }; InkRippleCtrl.prototype.isRippleAllowed = function () { var element = this.$element[0]; do { if (!element.tagName || element.tagName === 'BODY') break; if (element && angular.isFunction(element.hasAttribute)) { if (element.hasAttribute('disabled')) return false; if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false; } } while (element = element.parentNode); return true; }; /** * The attribute `md-ink-ripple` may be a static or interpolated * color value OR a boolean indicator (used to disable ripples) */ InkRippleCtrl.prototype.inkRipple = function () { return this.$element.attr('md-ink-ripple'); }; /** * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`. * @param left * @param top */ InkRippleCtrl.prototype.createRipple = function (left, top) { if (!this.isRippleAllowed()) return; var ctrl = this; var colorUtil = ctrl.$mdColorUtil; var ripple = angular.element('
'); var width = this.$element.prop('clientWidth'); var height = this.$element.prop('clientHeight'); var x = Math.max(Math.abs(width - left), left) * 2; var y = Math.max(Math.abs(height - top), top) * 2; var size = getSize(this.options.fitRipple, x, y); var color = this.calculateColor(); ripple.css({ left: left + 'px', top: top + 'px', background: 'black', width: size + 'px', height: size + 'px', backgroundColor: colorUtil.rgbaToRgb(color), borderColor: colorUtil.rgbaToRgb(color) }); this.lastRipple = ripple; // we only want one timeout to be running at a time this.clearTimeout(); this.timeout = this.$timeout(function () { ctrl.clearTimeout(); if (!ctrl.mousedown) ctrl.fadeInComplete(ripple); }, DURATION * 0.35, false); if (this.options.dimBackground) this.container.css({ backgroundColor: color }); this.container.append(ripple); this.ripples.push(ripple); ripple.addClass('md-ripple-placed'); this.$mdUtil.nextTick(function () { ripple.addClass('md-ripple-scaled md-ripple-active'); ctrl.$timeout(function () { ctrl.clearRipples(); }, DURATION, false); }, false); function getSize (fit, x, y) { return fit ? Math.max(x, y) : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); } }; /** * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup * @param ripple */ InkRippleCtrl.prototype.fadeInComplete = function (ripple) { if (this.lastRipple === ripple) { if (!this.timeout && !this.mousedown) { this.removeRipple(ripple); } } else { this.removeRipple(ripple); } }; /** * Kicks off the animation for removing a ripple * @param ripple {Element} */ InkRippleCtrl.prototype.removeRipple = function (ripple) { var ctrl = this; var index = this.ripples.indexOf(ripple); if (index < 0) return; this.ripples.splice(this.ripples.indexOf(ripple), 1); ripple.removeClass('md-ripple-active'); ripple.addClass('md-ripple-remove'); if (this.ripples.length === 0) this.container.css({ backgroundColor: '' }); // use a 2-second timeout in order to allow for the animation to finish // we don't actually care how long the animation takes this.$timeout(function () { ctrl.fadeOutComplete(ripple); }, DURATION, false); }; /** * Removes the provided ripple from the DOM * @param ripple */ InkRippleCtrl.prototype.fadeOutComplete = function (ripple) { ripple.remove(); this.lastRipple = null; }; /** * Used to create an empty directive. This is used to track flag-directives whose children may have * functionality based on them. * * Example: `md-no-ink` will potentially be used by all child directives. */ function attrNoDirective () { return { controller: angular.noop }; } })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdTabInkRipple * @module material.core * * @description * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ MdTabInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdTabInkRipple', MdTabInkRipple); function MdTabInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { return $mdInkRipple.attach(scope, element, angular.extend({ center: false, dimBackground: true, outline: false, rippleSize: 'full' }, options)); } } })(); })(); (function(){ "use strict"; angular.module('material.core.theming.palette', []) .constant('$mdColorPalette', { 'red': { '50': '#ffebee', '100': '#ffcdd2', '200': '#ef9a9a', '300': '#e57373', '400': '#ef5350', '500': '#f44336', '600': '#e53935', '700': '#d32f2f', '800': '#c62828', '900': '#b71c1c', 'A100': '#ff8a80', 'A200': '#ff5252', 'A400': '#ff1744', 'A700': '#d50000', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 A100', 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700' }, 'pink': { '50': '#fce4ec', '100': '#f8bbd0', '200': '#f48fb1', '300': '#f06292', '400': '#ec407a', '500': '#e91e63', '600': '#d81b60', '700': '#c2185b', '800': '#ad1457', '900': '#880e4f', 'A100': '#ff80ab', 'A200': '#ff4081', 'A400': '#f50057', 'A700': '#c51162', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100', 'contrastStrongLightColors': '500 600 A200 A400 A700' }, 'purple': { '50': '#f3e5f5', '100': '#e1bee7', '200': '#ce93d8', '300': '#ba68c8', '400': '#ab47bc', '500': '#9c27b0', '600': '#8e24aa', '700': '#7b1fa2', '800': '#6a1b9a', '900': '#4a148c', 'A100': '#ea80fc', 'A200': '#e040fb', 'A400': '#d500f9', 'A700': '#aa00ff', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100', 'contrastStrongLightColors': '300 400 A200 A400 A700' }, 'deep-purple': { '50': '#ede7f6', '100': '#d1c4e9', '200': '#b39ddb', '300': '#9575cd', '400': '#7e57c2', '500': '#673ab7', '600': '#5e35b1', '700': '#512da8', '800': '#4527a0', '900': '#311b92', 'A100': '#b388ff', 'A200': '#7c4dff', 'A400': '#651fff', 'A700': '#6200ea', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100', 'contrastStrongLightColors': '300 400 A200' }, 'indigo': { '50': '#e8eaf6', '100': '#c5cae9', '200': '#9fa8da', '300': '#7986cb', '400': '#5c6bc0', '500': '#3f51b5', '600': '#3949ab', '700': '#303f9f', '800': '#283593', '900': '#1a237e', 'A100': '#8c9eff', 'A200': '#536dfe', 'A400': '#3d5afe', 'A700': '#304ffe', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100', 'contrastStrongLightColors': '300 400 A200 A400' }, 'blue': { '50': '#e3f2fd', '100': '#bbdefb', '200': '#90caf9', '300': '#64b5f6', '400': '#42a5f5', '500': '#2196f3', '600': '#1e88e5', '700': '#1976d2', '800': '#1565c0', '900': '#0d47a1', 'A100': '#82b1ff', 'A200': '#448aff', 'A400': '#2979ff', 'A700': '#2962ff', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 400 A100', 'contrastStrongLightColors': '500 600 700 A200 A400 A700' }, 'light-blue': { '50': '#e1f5fe', '100': '#b3e5fc', '200': '#81d4fa', '300': '#4fc3f7', '400': '#29b6f6', '500': '#03a9f4', '600': '#039be5', '700': '#0288d1', '800': '#0277bd', '900': '#01579b', 'A100': '#80d8ff', 'A200': '#40c4ff', 'A400': '#00b0ff', 'A700': '#0091ea', 'contrastDefaultColor': 'dark', 'contrastLightColors': '600 700 800 900 A700', 'contrastStrongLightColors': '600 700 800 A700' }, 'cyan': { '50': '#e0f7fa', '100': '#b2ebf2', '200': '#80deea', '300': '#4dd0e1', '400': '#26c6da', '500': '#00bcd4', '600': '#00acc1', '700': '#0097a7', '800': '#00838f', '900': '#006064', 'A100': '#84ffff', 'A200': '#18ffff', 'A400': '#00e5ff', 'A700': '#00b8d4', 'contrastDefaultColor': 'dark', 'contrastLightColors': '700 800 900', 'contrastStrongLightColors': '700 800 900' }, 'teal': { '50': '#e0f2f1', '100': '#b2dfdb', '200': '#80cbc4', '300': '#4db6ac', '400': '#26a69a', '500': '#009688', '600': '#00897b', '700': '#00796b', '800': '#00695c', '900': '#004d40', 'A100': '#a7ffeb', 'A200': '#64ffda', 'A400': '#1de9b6', 'A700': '#00bfa5', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900', 'contrastStrongLightColors': '500 600 700' }, 'green': { '50': '#e8f5e9', '100': '#c8e6c9', '200': '#a5d6a7', '300': '#81c784', '400': '#66bb6a', '500': '#4caf50', '600': '#43a047', '700': '#388e3c', '800': '#2e7d32', '900': '#1b5e20', 'A100': '#b9f6ca', 'A200': '#69f0ae', 'A400': '#00e676', 'A700': '#00c853', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900', 'contrastStrongLightColors': '500 600 700' }, 'light-green': { '50': '#f1f8e9', '100': '#dcedc8', '200': '#c5e1a5', '300': '#aed581', '400': '#9ccc65', '500': '#8bc34a', '600': '#7cb342', '700': '#689f38', '800': '#558b2f', '900': '#33691e', 'A100': '#ccff90', 'A200': '#b2ff59', 'A400': '#76ff03', 'A700': '#64dd17', 'contrastDefaultColor': 'dark', 'contrastLightColors': '700 800 900', 'contrastStrongLightColors': '700 800 900' }, 'lime': { '50': '#f9fbe7', '100': '#f0f4c3', '200': '#e6ee9c', '300': '#dce775', '400': '#d4e157', '500': '#cddc39', '600': '#c0ca33', '700': '#afb42b', '800': '#9e9d24', '900': '#827717', 'A100': '#f4ff81', 'A200': '#eeff41', 'A400': '#c6ff00', 'A700': '#aeea00', 'contrastDefaultColor': 'dark', 'contrastLightColors': '900', 'contrastStrongLightColors': '900' }, 'yellow': { '50': '#fffde7', '100': '#fff9c4', '200': '#fff59d', '300': '#fff176', '400': '#ffee58', '500': '#ffeb3b', '600': '#fdd835', '700': '#fbc02d', '800': '#f9a825', '900': '#f57f17', 'A100': '#ffff8d', 'A200': '#ffff00', 'A400': '#ffea00', 'A700': '#ffd600', 'contrastDefaultColor': 'dark' }, 'amber': { '50': '#fff8e1', '100': '#ffecb3', '200': '#ffe082', '300': '#ffd54f', '400': '#ffca28', '500': '#ffc107', '600': '#ffb300', '700': '#ffa000', '800': '#ff8f00', '900': '#ff6f00', 'A100': '#ffe57f', 'A200': '#ffd740', 'A400': '#ffc400', 'A700': '#ffab00', 'contrastDefaultColor': 'dark' }, 'orange': { '50': '#fff3e0', '100': '#ffe0b2', '200': '#ffcc80', '300': '#ffb74d', '400': '#ffa726', '500': '#ff9800', '600': '#fb8c00', '700': '#f57c00', '800': '#ef6c00', '900': '#e65100', 'A100': '#ffd180', 'A200': '#ffab40', 'A400': '#ff9100', 'A700': '#ff6d00', 'contrastDefaultColor': 'dark', 'contrastLightColors': '800 900', 'contrastStrongLightColors': '800 900' }, 'deep-orange': { '50': '#fbe9e7', '100': '#ffccbc', '200': '#ffab91', '300': '#ff8a65', '400': '#ff7043', '500': '#ff5722', '600': '#f4511e', '700': '#e64a19', '800': '#d84315', '900': '#bf360c', 'A100': '#ff9e80', 'A200': '#ff6e40', 'A400': '#ff3d00', 'A700': '#dd2c00', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 400 A100 A200', 'contrastStrongLightColors': '500 600 700 800 900 A400 A700' }, 'brown': { '50': '#efebe9', '100': '#d7ccc8', '200': '#bcaaa4', '300': '#a1887f', '400': '#8d6e63', '500': '#795548', '600': '#6d4c41', '700': '#5d4037', '800': '#4e342e', '900': '#3e2723', 'A100': '#d7ccc8', 'A200': '#bcaaa4', 'A400': '#8d6e63', 'A700': '#5d4037', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100 A200', 'contrastStrongLightColors': '300 400' }, 'grey': { '50': '#fafafa', '100': '#f5f5f5', '200': '#eeeeee', '300': '#e0e0e0', '400': '#bdbdbd', '500': '#9e9e9e', '600': '#757575', '700': '#616161', '800': '#424242', '900': '#212121', 'A100': '#ffffff', 'A200': '#000000', 'A400': '#303030', 'A700': '#616161', 'contrastDefaultColor': 'dark', 'contrastLightColors': '600 700 800 900 A200 A400 A700' }, 'blue-grey': { '50': '#eceff1', '100': '#cfd8dc', '200': '#b0bec5', '300': '#90a4ae', '400': '#78909c', '500': '#607d8b', '600': '#546e7a', '700': '#455a64', '800': '#37474f', '900': '#263238', 'A100': '#cfd8dc', 'A200': '#b0bec5', 'A400': '#78909c', 'A700': '#455a64', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 A100 A200', 'contrastStrongLightColors': '400 500 700' } }); })(); (function(){ "use strict"; (function(angular) { 'use strict'; /** * @ngdoc module * @name material.core.theming * @description * Theming */ detectDisabledThemes.$inject = ["$mdThemingProvider"]; ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$parse", "$mdUtil", "$q", "$log"]; ThemableDirective.$inject = ["$mdTheming"]; ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"]; generateAllThemes.$inject = ["$injector", "$mdTheming"]; angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) .directive('mdTheme', ThemingDirective) .directive('mdThemable', ThemableDirective) .directive('mdThemesDisabled', disableThemesDirective ) .provider('$mdTheming', ThemingProvider) .config( detectDisabledThemes ) .run(generateAllThemes); /** * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute * If yes, then immediately disable all theme stylesheet generation and DOM injection */ /** * @ngInject */ function detectDisabledThemes($mdThemingProvider) { var isDisabled = !!document.querySelector('[md-themes-disabled]'); $mdThemingProvider.disableTheming(isDisabled); } /** * @ngdoc service * @name $mdThemingProvider * @module material.core.theming * * @description Provider to configure the `$mdTheming` service. * * ### Default Theme * The `$mdThemingProvider` uses by default the following theme configuration: * * - Primary Palette: `Blue` * - Accent Palette: `Pink` * - Warn Palette: `Deep-Orange` * - Background Palette: `Grey` * * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite * the default theme.
* This can be done by using the following markup. * * * myAppModule.config(function($mdThemingProvider) { * $mdThemingProvider * .theme('default') * .primaryPalette('blue') * .accentPalette('teal') * .warnPalette('red') * .backgroundPalette('grey'); * }); * * * ### Dynamic Themes * * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.
* If you have an application, which changes its theme on runtime, you have to enable theme watching. * * * myAppModule.config(function($mdThemingProvider) { * // Enable theme watching. * $mdThemingProvider.alwaysWatchTheme(true); * }); * * * ### Custom Theme Styles * * Sometimes you may want to use your own theme styles for some custom components.
* You are able to register your own styles by using the following markup. * * * myAppModule.config(function($mdThemingProvider) { * // Register our custom stylesheet into the theming provider. * $mdThemingProvider.registerStyles(STYLESHEET); * }); * * * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external * stylesheet file into the `$mdThemingProvider`. * * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content, * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`. * * * myAppModule.config(function($mdThemingProvider) { * // Register your custom stylesheet into the theming provider. * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css')); * }); * * * ### Browser color * * Enables browser header coloring * for more info please visit: * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color * * Options parameter:
* `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
* `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary`.
* `hue` - The hue from the selected palette. Default is `800`
* * * myAppModule.config(function($mdThemingProvider) { * // Enable browser color * $mdThemingProvider.enableBrowserColor({ * theme: 'myTheme', // Default is 'default' * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available * hue: '200' // Default is '800' * }); * }); * */ /** * @ngdoc method * @name $mdThemingProvider#registerStyles * @param {string} styles The styles to be appended to AngularJS Material's built in theme css. */ /** * @ngdoc method * @name $mdThemingProvider#setNonce * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. * Setting a value allows the use of CSP policy without using the unsafe-inline directive. */ /** * @ngdoc method * @name $mdThemingProvider#setDefaultTheme * @param {string} themeName Default theme name to be applied to elements. Default value is `default`. */ /** * @ngdoc method * @name $mdThemingProvider#alwaysWatchTheme * @param {boolean} watch Whether or not to always watch themes for changes and re-apply * classes when they change. Default is `false`. Enabling can reduce performance. */ /** * @ngdoc method * @name $mdThemingProvider#enableBrowserColor * @param {Object=} options Options object for the browser color
* `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
* `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary`.
* `hue` - The hue from the selected palette. Default is `800`
* @returns {Function} remove function of the browser color */ /* Some Example Valid Theming Expressions * ======================================= * * Intention group expansion: (valid for primary, accent, warn, background) * * {{primary-100}} - grab shade 100 from the primary palette * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 * {{primary-100-contrast}} - grab shade 100's contrast color * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules * * Foreground expansion: Applies rgba to black/white foreground text * * {{foreground-1}} - used for primary text * {{foreground-2}} - used for secondary text/divider * {{foreground-3}} - used for disabled text * {{foreground-4}} - used for dividers * */ // In memory generated CSS rules; registered by theme.name var GENERATED = { }; // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) var PALETTES; // Text Colors on light and dark backgrounds // @see https://www.google.com/design/spec/style/color.html#color-text-background-colors var DARK_FOREGROUND = { name: 'dark', '1': 'rgba(0,0,0,0.87)', '2': 'rgba(0,0,0,0.54)', '3': 'rgba(0,0,0,0.38)', '4': 'rgba(0,0,0,0.12)' }; var LIGHT_FOREGROUND = { name: 'light', '1': 'rgba(255,255,255,1.0)', '2': 'rgba(255,255,255,0.7)', '3': 'rgba(255,255,255,0.5)', '4': 'rgba(255,255,255,0.12)' }; var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; var LIGHT_SHADOW = ''; var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)'); var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; var DEFAULT_COLOR_TYPE = 'primary'; // A color in a theme will use these hues by default, if not specified by user. var LIGHT_DEFAULT_HUES = { 'accent': { 'default': 'A200', 'hue-1': 'A100', 'hue-2': 'A400', 'hue-3': 'A700' }, 'background': { 'default': '50', 'hue-1': 'A100', 'hue-2': '100', 'hue-3': '300' } }; var DARK_DEFAULT_HUES = { 'background': { 'default': 'A400', 'hue-1': '800', 'hue-2': '900', 'hue-3': 'A200' } }; THEME_COLOR_TYPES.forEach(function(colorType) { // Color types with unspecified default hues will use these default hue values var defaultDefaultHues = { 'default': '500', 'hue-1': '300', 'hue-2': '800', 'hue-3': 'A100' }; if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; }); var VALID_HUE_VALUES = [ '50', '100', '200', '300', '400', '500', '600', '700', '800', '900', 'A100', 'A200', 'A400', 'A700' ]; var themeConfig = { disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly). registeredStyles : [], // Custom styles registered to be used in the theming of custom components. nonce : null // Nonce to be added as an attribute to the generated themes style tags. }; /** * */ function ThemingProvider($mdColorPalette, $$mdMetaProvider) { ThemingService.$inject = ["$rootScope", "$mdUtil", "$q", "$log"]; PALETTES = { }; var THEMES = { }; var themingProvider; var alwaysWatchTheme = false; var defaultTheme = 'default'; // Load JS Defined Palettes angular.extend(PALETTES, $mdColorPalette); // Default theme defined in core.js /** * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter * @param {string} color Hex value of the wanted browser color * @returns {Function} Remove function of the meta tags */ var setBrowserColor = function (color) { // Chrome, Firefox OS and Opera var removeChrome = $$mdMetaProvider.setMeta('theme-color', color); // Windows Phone var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color); return function () { removeChrome(); removeWindows(); }; }; /** * Enables browser header coloring * for more info please visit: * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color * * The default color is `800` from `primary` palette of the `default` theme * * options are: * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary` * `hue` - The hue from the selected palette. Default is `800` * * @param {Object=} options Options object for the browser color * @returns {Function} remove function of the browser color */ var enableBrowserColor = function (options) { options = angular.isObject(options) ? options : {}; var theme = options.theme || 'default'; var hue = options.hue || '800'; var palette = PALETTES[options.palette] || PALETTES[THEMES[theme].colors[options.palette || 'primary'].name]; var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue]; return setBrowserColor(color); }; return themingProvider = { definePalette: definePalette, extendPalette: extendPalette, theme: registerTheme, /** * return a read-only clone of the current theme configuration */ configuration : function() { return angular.extend( { }, themeConfig, { defaultTheme : defaultTheme, alwaysWatchTheme : alwaysWatchTheme, registeredStyles : [].concat(themeConfig.registeredStyles) }); }, /** * Easy way to disable theming without having to use * `.constant("$MD_THEME_CSS","");` This disables * all dynamic theme style sheet generations and injections... */ disableTheming: function(isDisabled) { themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled; }, registerStyles: function(styles) { themeConfig.registeredStyles.push(styles); }, setNonce: function(nonceValue) { themeConfig.nonce = nonceValue; }, generateThemesOnDemand: function(onDemand) { themeConfig.generateOnDemand = onDemand; }, setDefaultTheme: function(theme) { defaultTheme = theme; }, alwaysWatchTheme: function(alwaysWatch) { alwaysWatchTheme = alwaysWatch; }, enableBrowserColor: enableBrowserColor, $get: ThemingService, _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, _PALETTES: PALETTES, _THEMES: THEMES, _parseRules: parseRules, _rgba: rgba }; // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); function definePalette(name, map) { map = map || {}; PALETTES[name] = checkPaletteValid(name, map); return themingProvider; } // Returns an new object which is a copy of a given palette `name` with variables from // `map` overwritten // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); function extendPalette(name, map) { return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); } // Make sure that palette has all required hues function checkPaletteValid(name, map) { var missingColors = VALID_HUE_VALUES.filter(function(field) { return !map[field]; }); if (missingColors.length) { throw new Error("Missing colors %1 in palette %2!" .replace('%1', missingColors.join(', ')) .replace('%2', name)); } return map; } // Register a theme (which is a collection of color palettes to use with various states // ie. warn, accent, primary ) // Optionally inherit from an existing theme // $mdThemingProvider.theme('custom-theme').primaryPalette('red'); function registerTheme(name, inheritFrom) { if (THEMES[name]) return THEMES[name]; inheritFrom = inheritFrom || 'default'; var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; var theme = new Theme(name); if (parentTheme) { angular.forEach(parentTheme.colors, function(color, colorType) { theme.colors[colorType] = { name: color.name, // Make sure a COPY of the hues is given to the child color, // not the same reference. hues: angular.extend({}, color.hues) }; }); } THEMES[name] = theme; return theme; } function Theme(name) { var self = this; self.name = name; self.colors = {}; self.dark = setDark; setDark(false); function setDark(isDark) { isDark = arguments.length === 0 ? true : !!isDark; // If no change, abort if (isDark === self.isDark) return; self.isDark = isDark; self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; // Light and dark themes have different default hues. // Go through each existing color type for this theme, and for every // hue value that is still the default hue value from the previous light/dark setting, // set it to the default hue value from the new light/dark setting. var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; angular.forEach(newDefaultHues, function(newDefaults, colorType) { var color = self.colors[colorType]; var oldDefaults = oldDefaultHues[colorType]; if (color) { for (var hueName in color.hues) { if (color.hues[hueName] === oldDefaults[hueName]) { color.hues[hueName] = newDefaults[hueName]; } } } }); return self; } THEME_COLOR_TYPES.forEach(function(colorType) { var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) { var color = self.colors[colorType] = { name: paletteName, hues: angular.extend({}, defaultHues, hues) }; Object.keys(color.hues).forEach(function(name) { if (!defaultHues[name]) { throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" .replace('%1', name) .replace('%2', self.name) .replace('%3', paletteName) .replace('%4', Object.keys(defaultHues).join(', ')) ); } }); Object.keys(color.hues).map(function(key) { return color.hues[key]; }).forEach(function(hueValue) { if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" .replace('%1', hueValue) .replace('%2', self.name) .replace('%3', colorType) .replace('%4', paletteName) .replace('%5', VALID_HUE_VALUES.join(', ')) ); } }); return self; }; self[colorType + 'Color'] = function() { var args = Array.prototype.slice.call(arguments); console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); return self[colorType + 'Palette'].apply(self, args); }; }); } /** * @ngdoc service * @name $mdTheming * @module material.core.theming * * @description * * Service that makes an element apply theming related classes to itself. * * * app.directive('myFancyDirective', function($mdTheming) { * return { * restrict: 'e', * link: function(scope, el, attrs) { * $mdTheming(el); * * $mdTheming.defineTheme('myTheme', { * primary: 'blue', * accent: 'pink', * dark: true * }) * } * }; * }); * * @param {element=} element to apply theming to */ /** * @ngdoc property * @name $mdTheming#THEMES * @description * Property to get all the themes defined * @returns {Object} All the themes defined with their properties */ /** * @ngdoc property * @name $mdTheming#PALETTES * @description * Property to get all the palettes defined * @returns {Object} All the palettes defined with their colors */ /** * @ngdoc method * @name $mdTheming#registered * @description * Determine is specified theme name is a valid, registered theme * @param {string} themeName the theme to check if registered * @returns {boolean} whether the theme is registered or not */ /** * @ngdoc method * @name $mdTheming#defaultTheme * @description * Returns the default theme * @returns {string} The default theme */ /** * @ngdoc method * @name $mdTheming#generateTheme * @description * Lazy generate themes - by default, every theme is generated when defined. * You can disable this in the configuration section using the * `$mdThemingProvider.generateThemesOnDemand(true);` * * The theme name that is passed in must match the name of the theme that was defined as part of the configuration block. * * @param name {string} theme name to generate */ /** * @ngdoc method * @name $mdTheming#setBrowserColor * @description * Sets browser header coloring * for more info please visit: * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color * * The default color is `800` from `primary` palette of the `default` theme * * options are:
* `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
* `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary`
* `hue` - The hue from the selected palette. Default is `800` * * @param {Object} options Options object for the browser color * @returns {Function} remove function of the browser color */ /** * @ngdoc method * @name $mdTheming#defineTheme * @description * Dynamically define a theme by an options object * * options are:
* `primary` - The primary palette of the theme.
* `accent` - The accent palette of the theme.
* `warn` - The warn palette of the theme.
* `background` - The background palette of the theme.
* `dark` - Indicates if it's a dark theme.
* * @param {String} name Theme name to define * @param {Object} options Theme definition options * @returns {Promise} A resolved promise with the theme name */ /* @ngInject */ function ThemingService($rootScope, $mdUtil, $q, $log) { // Allow us to be invoked via a linking function signature. var applyTheme = function (scope, el) { if (el === undefined) { el = scope; scope = undefined; } if (scope === undefined) { scope = $rootScope; } applyTheme.inherit(el, el); }; Object.defineProperty(applyTheme, 'THEMES', { get: function () { return angular.extend({}, THEMES); } }); Object.defineProperty(applyTheme, 'PALETTES', { get: function () { return angular.extend({}, PALETTES); } }); Object.defineProperty(applyTheme, 'ALWAYS_WATCH', { get: function () { return alwaysWatchTheme; } }); applyTheme.inherit = inheritTheme; applyTheme.registered = registered; applyTheme.defaultTheme = function() { return defaultTheme; }; applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; applyTheme.defineTheme = function(name, options) { options = options || {}; var theme = registerTheme(name); if (options.primary) { theme.primaryPalette(options.primary); } if (options.accent) { theme.accentPalette(options.accent); } if (options.warn) { theme.warnPalette(options.warn); } if (options.background) { theme.backgroundPalette(options.background); } if (options.dark){ theme.dark(); } this.generateTheme(name); return $q.resolve(name); }; applyTheme.setBrowserColor = enableBrowserColor; return applyTheme; /** * Determine is specified theme name is a valid, registered theme */ function registered(themeName) { if (themeName === undefined || themeName === '') return true; return applyTheme.THEMES[themeName] !== undefined; } /** * Get theme name for the element, then update with Theme CSS class */ function inheritTheme (el, parent) { var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); updateThemeClass(lookupThemeName()); if (ctrl) { var watchTheme = alwaysWatchTheme || ctrl.$shouldWatch || $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')); var unwatch = ctrl.registerChanges(function (name) { updateThemeClass(name); if (!watchTheme) { unwatch(); } else { el.on('$destroy', unwatch); } }); } /** * Find the theme name from the parent controller or element data */ function lookupThemeName() { // As a few components (dialog) add their controllers later, we should also watch for a controller init. return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); } /** * Remove old theme class and apply a new one * NOTE: if not a valid theme name, then the current name is not changed */ function updateThemeClass(theme) { if (!theme) return; if (!registered(theme)) { $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + 'Register it with $mdThemingProvider.theme().'); } var oldTheme = el.data('$mdThemeName'); if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); el.addClass('md-' + theme + '-theme'); el.data('$mdThemeName', theme); if (ctrl) { el.data('$mdThemeController', ctrl); } } } } } function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) { return { priority: 101, // has to be more than 100 to be before interpolation (issue on IE) link: { pre: function(scope, el, attrs) { var registeredCallbacks = []; var startSymbol = $interpolate.startSymbol(); var endSymbol = $interpolate.endSymbol(); var theme = attrs.mdTheme.trim(); var hasInterpolation = theme.substr(0, startSymbol.length) === startSymbol && theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length; var oneTimeOperator = '::'; var oneTimeBind = attrs.mdTheme .split(startSymbol).join('') .split(endSymbol).join('') .trim() .substr(0, oneTimeOperator.length) === oneTimeOperator; var ctrl = { registerChanges: function (cb, context) { if (context) { cb = angular.bind(context, cb); } registeredCallbacks.push(cb); return function () { var index = registeredCallbacks.indexOf(cb); if (index > -1) { registeredCallbacks.splice(index, 1); } }; }, $setTheme: function (theme) { if (!$mdTheming.registered(theme)) { $log.warn('attempted to use unregistered theme \'' + theme + '\''); } ctrl.$mdTheme = theme; // Iterating backwards to support unregistering during iteration // http://stackoverflow.com/a/9882349/890293 // we don't use `reverse()` of array because it mutates the array and we don't want it to get re-indexed for (var i = registeredCallbacks.length; i--;) { registeredCallbacks[i](theme); } }, $shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) || $mdTheming.ALWAYS_WATCH || (hasInterpolation && !oneTimeBind) }; el.data('$mdThemeController', ctrl); var getTheme = function () { var interpolation = $interpolate(attrs.mdTheme)(scope); return $parse(interpolation)(scope) || interpolation; }; var setParsedTheme = function (theme) { if (typeof theme === 'string') { return ctrl.$setTheme(theme); } $q.when( angular.isFunction(theme) ? theme() : theme ) .then(function(name){ ctrl.$setTheme(name); }); }; setParsedTheme(getTheme()); var unwatch = scope.$watch(getTheme, function(theme) { if (theme) { setParsedTheme(theme); if (!ctrl.$shouldWatch) { unwatch(); } } }); } } }; } /** * Special directive that will disable ALL runtime Theme style generation and DOM injection * * * * * * ... * * * Note: Using md-themes-css directive requires the developer to load external * theme stylesheets; e.g. custom themes from Material-Tools: * * `angular-material.themes.css` * * Another option is to use the ThemingProvider to configure and disable the attribute * conversions; this would obviate the use of the `md-themes-css` directive * */ function disableThemesDirective() { themeConfig.disableTheming = true; // Return a 1x-only, first-match attribute directive return { restrict : 'A', priority : '900' }; } function ThemableDirective($mdTheming) { return $mdTheming; } function parseRules(theme, colorType, rules) { checkValidPalette(theme, colorType); rules = rules.replace(/THEME_NAME/g, theme.name); var generatedRules = []; var color = theme.colors[colorType]; var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g'); // Matches '{{ primary-color }}', etc var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; var palette = PALETTES[color.name]; // find and replace simple variables where we use a specific hue, not an entire palette // eg. "{{primary-100}}" //\(' + THEME_COLOR_TYPES.join('\|') + '\)' rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { if (colorType === 'foreground') { if (hue == 'shadow') { return theme.foregroundShadow; } else { return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; } } // `default` is also accepted as a hue-value, because the background palettes are // using it as a name for the default hue. if (hue.indexOf('hue') === 0 || hue === 'default') { hue = theme.colors[colorType].hues[hue]; } return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); }); // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) angular.forEach(color.hues, function(hueValue, hueName) { var newRule = rules .replace(hueRegex, function(match, _, colorType, hueType, opacity) { return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); }); if (hueName !== 'default') { newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); } // Don't apply a selector rule to the default theme, making it easier to override // styles of the base-component if (theme.name == 'default') { var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; newRule = newRule.replace(themeRuleRegex, function(match, start, end) { return match + ', ' + start + end; }); } generatedRules.push(newRule); }); return generatedRules; } var rulesByType = {}; // Generate our themes at run time given the state of THEMES and PALETTES function generateAllThemes($injector, $mdTheming) { var head = document.head; var firstChild = head ? head.firstElementChild : null; var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; // Append our custom registered styles to the theme stylesheet. themeCss += themeConfig.registeredStyles.join(''); if ( !firstChild ) return; if (themeCss.length === 0) return; // no rules, so no point in running this expensive task // Expose contrast colors for palettes to ensure that text is always readable angular.forEach(PALETTES, sanitizePalette); // MD_THEME_CSS is a string generated by the build process that includes all the themable // components as templates // Break the CSS into individual rules var rules = themeCss .split(/\}(?!(\}|'|"|;))/) .filter(function(rule) { return rule && rule.trim().length; }) .map(function(rule) { return rule.trim() + '}'; }); var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); THEME_COLOR_TYPES.forEach(function(type) { rulesByType[type] = ''; }); // Sort the rules based on type, allowing us to do color substitution on a per-type basis rules.forEach(function(rule) { var match = rule.match(ruleMatchRegex); // First: test that if the rule has '.md-accent', it goes into the accent set of rules for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { if (rule.indexOf('.md-' + type) > -1) { return rulesByType[type] += rule; } } // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from // there for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { if (rule.indexOf(type) > -1) { return rulesByType[type] += rule; } } // Default to the primary array return rulesByType[DEFAULT_COLOR_TYPE] += rule; }); // If themes are being generated on-demand, quit here. The user will later manually // call generateTheme to do this on a theme-by-theme basis. if (themeConfig.generateOnDemand) return; angular.forEach($mdTheming.THEMES, function(theme) { if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { generateTheme(theme, theme.name, themeConfig.nonce); } }); // ************************* // Internal functions // ************************* // The user specifies a 'default' contrast color as either light or dark, // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) function sanitizePalette(palette, name) { var defaultContrast = palette.contrastDefaultColor; var lightColors = palette.contrastLightColors || []; var strongLightColors = palette.contrastStrongLightColors || []; var darkColors = palette.contrastDarkColors || []; // These colors are provided as space-separated lists if (typeof lightColors === 'string') lightColors = lightColors.split(' '); if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); if (typeof darkColors === 'string') darkColors = darkColors.split(' '); // Cleanup after ourselves delete palette.contrastDefaultColor; delete palette.contrastLightColors; delete palette.contrastStrongLightColors; delete palette.contrastDarkColors; // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } angular.forEach(palette, function(hueValue, hueName) { if (angular.isObject(hueValue)) return; // Already converted // Map everything to rgb colors var rgbValue = colorToRgbaArray(hueValue); if (!rgbValue) { throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." .replace('%1', hueValue) .replace('%2', palette.name) .replace('%3', hueName)); } palette[hueName] = { hex: palette[hueName], value: rgbValue, contrast: getContrastColor() }; function getContrastColor() { if (defaultContrast === 'light') { if (darkColors.indexOf(hueName) > -1) { return DARK_CONTRAST_COLOR; } else { return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } } else { if (lightColors.indexOf(hueName) > -1) { return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } else { return DARK_CONTRAST_COLOR; } } } }); } } function generateTheme(theme, name, nonce) { var head = document.head; var firstChild = head ? head.firstElementChild : null; if (!GENERATED[name]) { // For each theme, use the color palettes specified for // `primary`, `warn` and `accent` to generate CSS rules. THEME_COLOR_TYPES.forEach(function(colorType) { var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); while (styleStrings.length) { var styleContent = styleStrings.shift(); if (styleContent) { var style = document.createElement('style'); style.setAttribute('md-theme-style', ''); if (nonce) { style.setAttribute('nonce', nonce); } style.appendChild(document.createTextNode(styleContent)); head.insertBefore(style, firstChild); } } }); GENERATED[theme.name] = true; } } function checkValidPalette(theme, colorType) { // If theme attempts to use a palette that doesnt exist, throw error if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { throw new Error( "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" .replace('%1', theme.name) .replace('%2', colorType) .replace('%3', Object.keys(PALETTES).join(', ')) ); } } function colorToRgbaArray(clr) { if (angular.isArray(clr) && clr.length == 3) return clr; if (/^rgb/.test(clr)) { return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) { return i == 3 ? parseFloat(value, 10) : parseInt(value, 10); }); } if (clr.charAt(0) == '#') clr = clr.substring(1); if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; var dig = clr.length / 3; var red = clr.substr(0, dig); var grn = clr.substr(dig, dig); var blu = clr.substr(dig * 2); if (dig === 1) { red += red; grn += grn; blu += blu; } return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; } function rgba(rgbArray, opacity) { if ( !rgbArray ) return "rgb('0,0,0')"; if (rgbArray.length == 4) { rgbArray = angular.copy(rgbArray); opacity ? rgbArray.pop() : opacity = rgbArray.pop(); } return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : 'rgb(' + rgbArray.join(',') + ')'; } })(window.angular); })(); (function(){ "use strict"; // Polyfill angular < 1.4 (provide $animateCss) angular .module('material.core') .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){ // Since $$mdAnimate is injected into $mdUtil... use a wrapper function // to subsequently inject $mdUtil as an argument to the AnimateDomUtils return function($mdUtil) { return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss); }; }]); /** * Factory function that requires special injections */ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { var self; return self = { /** * */ translate3d : function( target, from, to, options ) { return $animateCss(target, { from: from, to: to, addClass: options.transitionInClass, removeClass: options.transitionOutClass, duration: options.duration }) .start() .then(function(){ // Resolve with reverser function... return reverseTranslate; }); /** * Specific reversal of the request translate animation above... */ function reverseTranslate (newFrom) { return $animateCss(target, { to: newFrom || from, addClass: options.transitionOutClass, removeClass: options.transitionInClass, duration: options.duration }).start(); } }, /** * Listen for transitionEnd event (with optional timeout) * Announce completion or failure via promise handlers */ waitTransitionEnd: function (element, opts) { var TIMEOUT = 3000; // fallback is 3 secs return $q(function(resolve, reject){ opts = opts || { }; // If there is no transition is found, resolve immediately // // NOTE: using $mdUtil.nextTick() causes delays/issues if (noTransitionFound(opts.cachedTransitionStyles)) { TIMEOUT = 0; } var timer = $timeout(finished, opts.timeout || TIMEOUT); element.on($mdConstant.CSS.TRANSITIONEND, finished); /** * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. * NOTE: Make sure this transitionEnd didn't bubble up from a child */ function finished(ev) { if ( ev && ev.target !== element[0]) return; if ( ev ) $timeout.cancel(timer); element.off($mdConstant.CSS.TRANSITIONEND, finished); // Never reject since ngAnimate may cause timeouts due missed transitionEnd events resolve(); } /** * Checks whether or not there is a transition. * * @param styles The cached styles to use for the calculation. If null, getComputedStyle() * will be used. * * @returns {boolean} True if there is no transition/duration; false otherwise. */ function noTransitionFound(styles) { styles = styles || window.getComputedStyle(element[0]); return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty); } }); }, calculateTransformValues: function (element, originator) { var origin = originator.element; var bounds = originator.bounds; if (origin || bounds) { var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); var dialogRect = self.copyRect(element[0].getBoundingClientRect()); var dialogCenterPt = self.centerPointFor(dialogRect); var originCenterPt = self.centerPointFor(originBnds); return { centerX: originCenterPt.x - dialogCenterPt.x, centerY: originCenterPt.y - dialogCenterPt.y, scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100, scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100 }; } return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}; /** * This is a fallback if the origin information is no longer valid, then the * origin bounds simply becomes the current bounds for the dialogContainer's parent */ function currentBounds() { var cntr = element ? element.parent() : null; var parent = cntr ? cntr.parent() : null; return parent ? self.clientRect(parent) : null; } }, /** * Calculate the zoom transform from dialog to origin. * * We use this to set the dialog position immediately; * then the md-transition-in actually translates back to * `translate3d(0,0,0) scale(1.0)`... * * NOTE: all values are rounded to the nearest integer */ calculateZoomToOrigin: function (element, originator) { var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); return buildZoom(self.calculateTransformValues(element, originator)); }, /** * Calculate the slide transform from panel to origin. * NOTE: all values are rounded to the nearest integer */ calculateSlideToOrigin: function (element, originator) { var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )"; var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate); return buildSlide(self.calculateTransformValues(element, originator)); }, /** * Enhance raw values to represent valid css stylings... */ toCss : function( raw ) { var css = { }; var lookups = 'left top right bottom width height x y min-width min-height max-width max-height'; angular.forEach(raw, function(value,key) { if ( angular.isUndefined(value) ) return; if ( lookups.indexOf(key) >= 0 ) { css[key] = value + 'px'; } else { switch (key) { case 'transition': convertToVendor(key, $mdConstant.CSS.TRANSITION, value); break; case 'transform': convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); break; case 'transformOrigin': convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); break; case 'font-size': css['font-size'] = value; // font sizes aren't always in px break; } } }); return css; function convertToVendor(key, vendor, value) { angular.forEach(vendor.split(' '), function (key) { css[key] = value; }); } }, /** * Convert the translate CSS value to key/value pair(s). */ toTransformCss: function (transform, addTransition, transition) { var css = {}; angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) { css[key] = transform; }); if (addTransition) { transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; css.transition = transition; } return css; }, /** * Clone the Rect and calculate the height/width if needed */ copyRect: function (source, destination) { if (!source) return null; destination = destination || {}; angular.forEach('left top right bottom width height'.split(' '), function (key) { destination[key] = Math.round(source[key]); }); destination.width = destination.width || (destination.right - destination.left); destination.height = destination.height || (destination.bottom - destination.top); return destination; }, /** * Calculate ClientRect of element; return null if hidden or zero size */ clientRect: function (element) { var bounds = angular.element(element)[0].getBoundingClientRect(); var isPositiveSizeClientRect = function (rect) { return rect && (rect.width > 0) && (rect.height > 0); }; // If the event origin element has zero size, it has probably been hidden. return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null; }, /** * Calculate 'rounded' center point of Rect */ centerPointFor: function (targetRect) { return targetRect ? { x: Math.round(targetRect.left + (targetRect.width / 2)), y: Math.round(targetRect.top + (targetRect.height / 2)) } : { x : 0, y : 0 }; } }; } })(); (function(){ "use strict"; if (angular.version.minor >= 4) { angular.module('material.core.animate', []); } else { (function() { "use strict"; var forEach = angular.forEach; var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance); var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition'; var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation'; var PREFIX = WEBKIT ? '-webkit-' : ''; var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend'; var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend'; var $$ForceReflowFactory = ['$document', function($document) { return function() { return $document[0].body.clientWidth + 1; }; }]; var $$rAFMutexFactory = ['$$rAF', function($$rAF) { return function() { var passed = false; $$rAF(function() { passed = true; }); return function(fn) { passed ? fn() : $$rAF(fn); }; }; }]; var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { var INITIAL_STATE = 0; var DONE_PENDING_STATE = 1; var DONE_COMPLETE_STATE = 2; function AnimateRunner(host) { this.setHost(host); this._doneCallbacks = []; this._runInAnimationFrame = $$rAFMutex(); this._state = 0; } AnimateRunner.prototype = { setHost: function(host) { this.host = host || {}; }, done: function(fn) { if (this._state === DONE_COMPLETE_STATE) { fn(); } else { this._doneCallbacks.push(fn); } }, progress: angular.noop, getPromise: function() { if (!this.promise) { var self = this; this.promise = $q(function(resolve, reject) { self.done(function(status) { status === false ? reject() : resolve(); }); }); } return this.promise; }, then: function(resolveHandler, rejectHandler) { return this.getPromise().then(resolveHandler, rejectHandler); }, 'catch': function(handler) { return this.getPromise()['catch'](handler); }, 'finally': function(handler) { return this.getPromise()['finally'](handler); }, pause: function() { if (this.host.pause) { this.host.pause(); } }, resume: function() { if (this.host.resume) { this.host.resume(); } }, end: function() { if (this.host.end) { this.host.end(); } this._resolve(true); }, cancel: function() { if (this.host.cancel) { this.host.cancel(); } this._resolve(false); }, complete: function(response) { var self = this; if (self._state === INITIAL_STATE) { self._state = DONE_PENDING_STATE; self._runInAnimationFrame(function() { self._resolve(response); }); } }, _resolve: function(response) { if (this._state !== DONE_COMPLETE_STATE) { forEach(this._doneCallbacks, function(fn) { fn(response); }); this._doneCallbacks.length = 0; this._state = DONE_COMPLETE_STATE; } } }; // Polyfill AnimateRunner.all which is used by input animations AnimateRunner.all = function(runners, callback) { var count = 0; var status = true; forEach(runners, function(runner) { runner.done(onProgress); }); function onProgress(response) { status = status && response; if (++count === runners.length) { callback(status); } } }; return AnimateRunner; }]; angular .module('material.core.animate', []) .factory('$$forceReflow', $$ForceReflowFactory) .factory('$$AnimateRunner', $$AnimateRunnerFactory) .factory('$$rAFMutex', $$rAFMutexFactory) .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate', function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) { function init(element, options) { var temporaryStyles = []; var node = getDomNode(element); var areAnimationsAllowed = node && $animate.enabled(); var hasCompleteStyles = false; var hasCompleteClasses = false; if (areAnimationsAllowed) { if (options.transitionStyle) { temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); } if (options.keyframeStyle) { temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); } if (options.delay) { temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); } if (options.duration) { temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); } hasCompleteStyles = options.keyframeStyle || (options.to && (options.duration > 0 || options.transitionStyle)); hasCompleteClasses = !!options.addClass || !!options.removeClass; blockTransition(element, true); } var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses); applyAnimationFromStyles(element, options); var animationClosed = false; var events, eventFn; return { close: $window.close, start: function() { var runner = new $$AnimateRunner(); waitUntilQuiet(function() { blockTransition(element, false); if (!hasCompleteAnimation) { return close(); } forEach(temporaryStyles, function(entry) { var key = entry[0]; var value = entry[1]; node.style[camelCase(key)] = value; }); applyClasses(element, options); var timings = computeTimings(element); if (timings.duration === 0) { return close(); } var moreStyles = []; if (options.easing) { if (timings.transitionDuration) { moreStyles.push([PREFIX + 'transition-timing-function', options.easing]); } if (timings.animationDuration) { moreStyles.push([PREFIX + 'animation-timing-function', options.easing]); } } if (options.delay && timings.animationDelay) { moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']); } if (options.duration && timings.animationDuration) { moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']); } forEach(moreStyles, function(entry) { var key = entry[0]; var value = entry[1]; node.style[camelCase(key)] = value; temporaryStyles.push(entry); }); var maxDelay = timings.delay; var maxDelayTime = maxDelay * 1000; var maxDuration = timings.duration; var maxDurationTime = maxDuration * 1000; var startTime = Date.now(); events = []; if (timings.transitionDuration) { events.push(TRANSITION_EVENTS); } if (timings.animationDuration) { events.push(ANIMATION_EVENTS); } events = events.join(' '); eventFn = function(event) { event.stopPropagation(); var ev = event.originalEvent || event; var timeStamp = ev.timeStamp || Date.now(); var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3)); if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { close(); } }; element.on(events, eventFn); applyAnimationToStyles(element, options); $timeout(close, maxDelayTime + maxDurationTime * 1.5, false); }); return runner; function close() { if (animationClosed) return; animationClosed = true; if (events && eventFn) { element.off(events, eventFn); } applyClasses(element, options); applyAnimationStyles(element, options); forEach(temporaryStyles, function(entry) { node.style[camelCase(entry[0])] = ''; }); runner.complete(true); return runner; } } }; } function applyClasses(element, options) { if (options.addClass) { $$jqLite.addClass(element, options.addClass); options.addClass = null; } if (options.removeClass) { $$jqLite.removeClass(element, options.removeClass); options.removeClass = null; } } function computeTimings(element) { var node = getDomNode(element); var cs = $window.getComputedStyle(node); var tdr = parseMaxTime(cs[prop('transitionDuration')]); var adr = parseMaxTime(cs[prop('animationDuration')]); var tdy = parseMaxTime(cs[prop('transitionDelay')]); var ady = parseMaxTime(cs[prop('animationDelay')]); adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1); var duration = Math.max(adr, tdr); var delay = Math.max(ady, tdy); return { duration: duration, delay: delay, animationDuration: adr, transitionDuration: tdr, animationDelay: ady, transitionDelay: tdy }; function prop(key) { return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1) : key; } } function parseMaxTime(str) { var maxValue = 0; var values = (str || "").split(/\s*,\s*/); forEach(values, function(value) { // it's always safe to consider only second values and omit `ms` values since // getComputedStyle will always handle the conversion for us if (value.charAt(value.length - 1) == 's') { value = value.substring(0, value.length - 1); } value = parseFloat(value) || 0; maxValue = maxValue ? Math.max(value, maxValue) : value; }); return maxValue; } var cancelLastRAFRequest; var rafWaitQueue = []; function waitUntilQuiet(callback) { if (cancelLastRAFRequest) { cancelLastRAFRequest(); //cancels the request } rafWaitQueue.push(callback); cancelLastRAFRequest = $$rAF(function() { cancelLastRAFRequest = null; // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. // PLEASE EXAMINE THE `$$forceReflow` service to understand why. var pageWidth = $$forceReflow(); // we use a for loop to ensure that if the queue is changed // during this looping then it will consider new requests for (var i = 0; i < rafWaitQueue.length; i++) { rafWaitQueue[i](pageWidth); } rafWaitQueue.length = 0; }); } function applyAnimationStyles(element, options) { applyAnimationFromStyles(element, options); applyAnimationToStyles(element, options); } function applyAnimationFromStyles(element, options) { if (options.from) { element.css(options.from); options.from = null; } } function applyAnimationToStyles(element, options) { if (options.to) { element.css(options.to); options.to = null; } } function getDomNode(element) { for (var i = 0; i < element.length; i++) { if (element[i].nodeType === 1) return element[i]; } } function blockTransition(element, bool) { var node = getDomNode(element); var key = camelCase(PREFIX + 'transition-delay'); node.style[key] = bool ? '-9999s' : ''; } return init; }]); /** * Older browsers [FF31] expect camelCase * property keys. * e.g. * animation-duration --> animationDuration */ function camelCase(str) { return str.replace(/-[a-z]/g, function(str) { return str.charAt(1).toUpperCase(); }); } })(); } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.autocomplete */ /* * @see js folder for autocomplete implementation */ angular.module('material.components.autocomplete', [ 'material.core', 'material.components.icon', 'material.components.virtualRepeat' ]); })(); (function(){ "use strict"; /* * @ngdoc module * @name material.components.backdrop * @description Backdrop */ /** * @ngdoc directive * @name mdBackdrop * @module material.components.backdrop * * @restrict E * * @description * `` is a backdrop element used by other components, such as dialog and bottom sheet. * Apply class `opaque` to make the backdrop use the theme backdrop color. * */ angular .module('material.components.backdrop', ['material.core']) .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) { var ERROR_CSS_POSITION = ' may not work properly in a scrolled, static-positioned parent container.'; return { restrict: 'E', link: postLink }; function postLink(scope, element, attrs) { // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless if ($animate.pin) $animate.pin(element, $rootElement); var bodyStyles; $$rAF(function() { // If body scrolling has been disabled using mdUtil.disableBodyScroll(), // adjust the 'backdrop' height to account for the fixed 'body' top offset. // Note that this can be pretty expensive and is better done inside the $$rAF. bodyStyles = $window.getComputedStyle($document[0].body); if (bodyStyles.position === 'fixed') { var resizeHandler = $mdUtil.debounce(function(){ bodyStyles = $window.getComputedStyle($document[0].body); resize(); }, 60, null, false); resize(); angular.element($window).on('resize', resizeHandler); scope.$on('$destroy', function() { angular.element($window).off('resize', resizeHandler); }); } // Often $animate.enter() is used to append the backDrop element // so let's wait until $animate is done... var parent = element.parent(); if (parent.length) { if (parent[0].nodeName === 'BODY') { element.css('position', 'fixed'); } var styles = $window.getComputedStyle(parent[0]); if (styles.position === 'static') { // backdrop uses position:absolute and will not work properly with parent position:static (default) $log.warn(ERROR_CSS_POSITION); } // Only inherit the parent if the backdrop has a parent. $mdTheming.inherit(element, parent); } }); function resize() { var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10)); element.css('height', viewportHeight + 'px'); } } }]); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.bottomSheet * @description * BottomSheet */ MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; angular .module('material.components.bottomSheet', [ 'material.core', 'material.components.backdrop' ]) .directive('mdBottomSheet', MdBottomSheetDirective) .provider('$mdBottomSheet', MdBottomSheetProvider); /* @ngInject */ function MdBottomSheetDirective($mdBottomSheet) { return { restrict: 'E', link : function postLink(scope, element) { element.addClass('_md'); // private md component indicator for styling // When navigation force destroys an interimElement, then // listen and $destroy() that interim instance... scope.$on('$destroy', function() { $mdBottomSheet.destroy(); }); } }; } /** * @ngdoc service * @name $mdBottomSheet * @module material.components.bottomSheet * * @description * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API. * * ## Restrictions * * - The bottom sheet's template must have an outer `` element. * - Add the `md-grid` class to the bottom sheet for a grid layout. * - Add the `md-list` class to the bottom sheet for a list layout. * * @usage * *
* * Open a Bottom Sheet! * *
*
* * var app = angular.module('app', ['ngMaterial']); * app.controller('MyController', function($scope, $mdBottomSheet) { * $scope.openBottomSheet = function() { * $mdBottomSheet.show({ * template: '' + * 'Hello! Close' + * '' * }) * * // Fires when the hide() method is used * .then(function() { * console.log('You clicked the button to close the bottom sheet!'); * }) * * // Fires when the cancel() method is used * .catch(function() { * console.log('You hit escape or clicked the backdrop to close.'); * }); * }; * * $scope.closeBottomSheet = function($scope, $mdBottomSheet) { * $mdBottomSheet.hide(); * } * * }); * */ /** * @ngdoc method * @name $mdBottomSheet#show * * @description * Show a bottom sheet with the specified options. * * Note: You should always provide a `.catch()` method in case the user hits the * `esc` key or clicks the background to close. In this case, the `cancel()` method will * automatically be called on the bottom sheet which will `reject()` the promise. See the @usage * section above for an example. * * Newer versions of Angular will throw a `Possibly unhandled rejection` exception if you forget * this. * * @param {object} options An options object, with the following properties: * * - `templateUrl` - `{string=}`: The url of an html template file that will * be used as the content of the bottom sheet. Restrictions: the template must * have an outer `md-bottom-sheet` element. * - `template` - `{string=}`: Same as templateUrl, except this is an actual * template string. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false * - `controller` - `{string=}`: The controller to associate with this bottom sheet. * - `locals` - `{string=}`: An object containing key/value pairs. The keys will * be used as names of values to inject into the controller. For example, * `locals: {three: 3}` would inject `three` into the controller with the value * of 3. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to * close it. Default true. * - `bindToController` - `{boolean=}`: When set to true, the locals will be bound to the controller instance. * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. * Default true. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values * and the bottom sheet will not open until the promises resolve. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`, * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application. * e.g. angular.element(document.getElementById('content')) or "#content" * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open. * Default true. * * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or * rejected with `$mdBottomSheet.cancel()`. */ /** * @ngdoc method * @name $mdBottomSheet#hide * * @description * Hide the existing bottom sheet and resolve the promise returned from * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if * any). * * Note: Use a `.then()` on your `.show()` to handle this callback. * * @param {*=} response An argument for the resolved promise. * */ /** * @ngdoc method * @name $mdBottomSheet#cancel * * @description * Hide the existing bottom sheet and reject the promise returned from * `$mdBottomSheet.show()`. * * Note: Use a `.catch()` on your `.show()` to handle this callback. * * @param {*=} response An argument for the rejected promise. * */ function MdBottomSheetProvider($$interimElementProvider) { // how fast we need to flick down to close the sheet, pixels/ms bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"]; var CLOSING_VELOCITY = 0.5; var PADDING = 80; // same as css return $$interimElementProvider('$mdBottomSheet') .setDefaults({ methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'], options: bottomSheetDefaults }); /* @ngInject */ function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture, $log) { var backdrop; return { themable: true, onShow: onShow, onRemove: onRemove, disableBackdrop: false, escapeToClose: true, clickOutsideToClose: true, disableParentScroll: true }; function onShow(scope, element, options, controller) { element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); // prevent tab focus or click focus on the bottom-sheet container element.attr('tabindex',"-1"); // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly. // This is a very common problem, so we have to notify the developer about this. if (element.hasClass('ng-cloak')) { var message = '$mdBottomSheet: using `` will affect the bottom-sheet opening animations.'; $log.warn( message, element[0] ); } if (!options.disableBackdrop) { // Add a backdrop that will close on click backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque"); // Prevent mouse focus on backdrop; ONLY programatic focus allowed. // This allows clicks on backdrop to propogate to the $rootElement and // ESC key events to be detected properly. backdrop[0].tabIndex = -1; if (options.clickOutsideToClose) { backdrop.on('click', function() { $mdUtil.nextTick($mdBottomSheet.cancel,true); }); } $mdTheming.inherit(backdrop, options.parent); $animate.enter(backdrop, options.parent, null); } var bottomSheet = new BottomSheet(element, options.parent); options.bottomSheet = bottomSheet; $mdTheming.inherit(bottomSheet.element, options.parent); if (options.disableParentScroll) { options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent); } return $animate.enter(bottomSheet.element, options.parent, backdrop) .then(function() { var focusable = $mdUtil.findFocusTarget(element) || angular.element( element[0].querySelector('button') || element[0].querySelector('a') || element[0].querySelector($mdUtil.prefixer('ng-click', true)) ) || backdrop; if (options.escapeToClose) { options.rootElementKeyupCallback = function(e) { if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) { $mdUtil.nextTick($mdBottomSheet.cancel,true); } }; $rootElement.on('keyup', options.rootElementKeyupCallback); focusable && focusable.focus(); } }); } function onRemove(scope, element, options) { var bottomSheet = options.bottomSheet; if (!options.disableBackdrop) $animate.leave(backdrop); return $animate.leave(bottomSheet.element).then(function() { if (options.disableParentScroll) { options.restoreScroll(); delete options.restoreScroll; } bottomSheet.cleanup(); }); } /** * BottomSheet class to apply bottom-sheet behavior to an element */ function BottomSheet(element, parent) { var deregister = $mdGesture.register(parent, 'drag', { horizontal: false }); parent.on('$md.dragstart', onDragStart) .on('$md.drag', onDrag) .on('$md.dragend', onDragEnd); return { element: element, cleanup: function cleanup() { deregister(); parent.off('$md.dragstart', onDragStart); parent.off('$md.drag', onDrag); parent.off('$md.dragend', onDragEnd); } }; function onDragStart(ev) { // Disable transitions on transform so that it feels fast element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); } function onDrag(ev) { var transform = ev.pointer.distanceY; if (transform < 5) { // Slow down drag when trying to drag up, and stop after PADDING transform = Math.max(-PADDING, transform / 2); } element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)'); } function onDragEnd(ev) { if (ev.pointer.distanceY > 0 && (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) { var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY; var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500); element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms'); $mdUtil.nextTick($mdBottomSheet.cancel,true); } else { element.css($mdConstant.CSS.TRANSITION_DURATION, ''); element.css($mdConstant.CSS.TRANSFORM, ''); } } } } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.button * @description * * Button */ MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$mdInteraction"]; MdAnchorDirective.$inject = ["$mdTheming"]; angular .module('material.components.button', [ 'material.core' ]) .directive('mdButton', MdButtonDirective) .directive('a', MdAnchorDirective); /** * @private * @restrict E * * @description * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc. * * @usage * * * * * * */ function MdAnchorDirective($mdTheming) { return { restrict : 'E', link : function postLink(scope, element) { // Make sure to inherit theme so stand-alone anchors // support theme colors for md-primary, md-accent, etc. $mdTheming(element); } }; } /** * @ngdoc directive * @name mdButton * @module material.components.button * * @restrict E * * @description * `` is a button directive with optional ink ripples (default enabled). * * If you supply a `href` or `ng-href` attribute, it will become an `` element. Otherwise, it * will become a `'; } } function postLink(scope, element, attr) { $mdTheming(element); $mdButtonInkRipple.attach(scope, element); // Use async expect to support possible bindings in the button label $mdAria.expectWithoutText(element, 'aria-label'); // For anchor elements, we have to set tabindex manually when the // element is disabled if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { scope.$watch(attr.ngDisabled, function(isDisabled) { element.attr('tabindex', isDisabled ? -1 : 0); }); } // disabling click event when disabled is true element.on('click', function(e){ if (attr.disabled === true) { e.preventDefault(); e.stopImmediatePropagation(); } }); if (!element.hasClass('md-no-focus')) { element.on('focus', function() { // Only show the focus effect when being focused through keyboard interaction or programmatically if (!$mdInteraction.isUserInvoked() || $mdInteraction.getLastInteractionType() === 'keyboard') { element.addClass('md-focused'); } }); element.on('blur', function() { element.removeClass('md-focused'); }); } } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.card * * @description * Card components. */ mdCardDirective.$inject = ["$mdTheming"]; angular.module('material.components.card', [ 'material.core' ]) .directive('mdCard', mdCardDirective); /** * @ngdoc directive * @name mdCard * @module material.components.card * * @restrict E * * @description * The `` directive is a container element used within `` containers. * * An image included as a direct descendant will fill the card's width. If you want to avoid this, * you can add the `md-image-no-fill` class to the parent element. The `` * container will wrap text content and provide padding. An `` element can be * optionally included to put content flush against the bottom edge of the card. * * Action buttons can be included in an `` element, similar to ``. * You can then position buttons using layout attributes. * * Card is built with: * * `` - Header for the card, holds avatar, text and squared image * - `` - Card avatar * - `md-user-avatar` - Class for user image * - `` * - `` - Contains elements for the card description * - `md-title` - Class for the card title * - `md-subhead` - Class for the card sub header * * `` - Image for the card * * `` - Card content title * - `` * - `md-headline` - Class for the card content title * - `md-subhead` - Class for the card content sub header * - `` - Squared image within the title * - `md-media-sm` - Class for small image * - `md-media-md` - Class for medium image * - `md-media-lg` - Class for large image * - `md-media-xl` - Class for extra large image * * `` - Card content * * `` - Card actions * - `` - Icon actions * * Cards have constant width and variable heights; where the maximum height is limited to what can * fit within a single view on a platform, but it can temporarily expand as needed. * * @usage * ### Card with optional footer * * * image caption * *

Card headline

*

Card content

*
* * Card footer * *
*
* * ### Card with actions * * * image caption * *

Card headline

*

Card content

*
* * Action 1 * Action 2 * *
*
* * ### Card with header, image, title actions and content * * * * * * * * Title * Sub header * * * image caption * * * Card headline * Card subheader * * * * Action 1 * Action 2 * * * * * * * *

* Card content *

*
*
*
*/ function mdCardDirective($mdTheming) { return { restrict: 'E', link: function ($scope, $element, attr) { $element.addClass('_md'); // private md component indicator for styling $mdTheming($element); } }; } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.chips */ /* * @see js folder for chips implementation */ angular.module('material.components.chips', [ 'material.core', 'material.components.autocomplete' ]); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.checkbox * @description Checkbox module! */ MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$mdInteraction"]; angular .module('material.components.checkbox', ['material.core']) .directive('mdCheckbox', MdCheckboxDirective); /** * @ngdoc directive * @name mdCheckbox * @module material.components.checkbox * @restrict E * * @description * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). * * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-color-schemes) * the checkbox is in the accent color by default. The primary color palette may be used with * the `md-primary` class. * * @param {string} ng-model Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {expression=} ng-true-value The value to which the expression should be set when selected. * @param {expression=} ng-false-value The value to which the expression should be set when not selected. * @param {string=} ng-change AngularJS expression to be executed when input changes due to user interaction with the input element. * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects * @param {string=} aria-label Adds label to checkbox for accessibility. * Defaults to checkbox's text. If no default text is found, a warning will be logged. * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'. * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state. * If falsy expression is passed in it just looks like a normal unchecked checkbox. * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time. * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic. * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'. * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it * will appear checked. * * @usage * * * Finished ? * * * * No Ink Effects * * * * Disabled * * * * */ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $mdInteraction) { inputDirective = inputDirective[0]; return { restrict: 'E', transclude: true, require: ['^?mdInputContainer', '?ngModel', '?^form'], priority: $mdConstant.BEFORE_NG_ARIA, template: '
' + '
' + '
' + '
', compile: compile }; // ********************************************************** // Private Methods // ********************************************************** function compile (tElement, tAttrs) { tAttrs.$set('tabindex', tAttrs.tabindex || '0'); tAttrs.$set('type', 'checkbox'); tAttrs.$set('role', tAttrs.type); return { pre: function(scope, element) { // Attach a click handler during preLink, in order to immediately stop propagation // (especially for ng-click) when the checkbox is disabled. element.on('click', function(e) { if (this.hasAttribute('disabled')) { e.stopImmediatePropagation(); } }); }, post: postLink }; function postLink(scope, element, attr, ctrls) { var isIndeterminate; var containerCtrl = ctrls[0]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); var formCtrl = ctrls[2]; if (containerCtrl) { var isErrorGetter = containerCtrl.isErrorGetter || function() { return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted)); }; containerCtrl.input = element; scope.$watch(isErrorGetter, containerCtrl.setInvalid); } $mdTheming(element); // Redirect focus events to the root element, because IE11 is always focusing the container element instead // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur` element.children().on('focus', function() { element.focus(); }); if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) { setIndeterminateState(); scope.$watch(attr.mdIndeterminate, setIndeterminateState); } if (attr.ngChecked) { scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) { ngModelCtrl.$setViewValue(value); ngModelCtrl.$render(); }); } $$watchExpr('ngDisabled', 'tabindex', { true: '-1', false: attr.tabindex }); $mdAria.expectWithText(element, 'aria-label'); // Reuse the original input[type=checkbox] directive from AngularJS core. // This is a bit hacky as we need our own event listener and own render // function. inputDirective.link.pre(scope, { on: angular.noop, 0: {} }, attr, [ngModelCtrl]); element.on('click', listener) .on('keypress', keypressHandler) .on('focus', function() { if ($mdInteraction.getLastInteractionType() === 'keyboard') { element.addClass('md-focused'); } }) .on('blur', function() { element.removeClass('md-focused'); }); ngModelCtrl.$render = render; function $$watchExpr(expr, htmlAttr, valueOpts) { if (attr[expr]) { scope.$watch(attr[expr], function(val) { if (valueOpts[val]) { element.attr(htmlAttr, valueOpts[val]); } }); } } function keypressHandler(ev) { var keyCode = ev.which || ev.keyCode; if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) { ev.preventDefault(); element.addClass('md-focused'); listener(ev); } } function listener(ev) { // skipToggle boolean is used by the switch directive to prevent the click event // when releasing the drag. There will be always a click if releasing the drag over the checkbox if (element[0].hasAttribute('disabled') || scope.skipToggle) { return; } scope.$apply(function() { // Toggle the checkbox value... var viewValue = attr.ngChecked && attr.ngClick ? attr.checked : !ngModelCtrl.$viewValue; ngModelCtrl.$setViewValue(viewValue, ev && ev.type); ngModelCtrl.$render(); }); } function render() { // Cast the $viewValue to a boolean since it could be undefined element.toggleClass('md-checked', !!ngModelCtrl.$viewValue && !isIndeterminate); } function setIndeterminateState(newValue) { isIndeterminate = newValue !== false; if (isIndeterminate) { element.attr('aria-checked', 'mixed'); } element.toggleClass('md-indeterminate', isIndeterminate); } } } } })(); (function(){ "use strict"; (function () { "use strict"; /** * Use a RegExp to check if the `md-colors=""` is static string * or one that should be observed and dynamically interpolated. */ MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"]; MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"]; var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/; var colorPalettes = null; /** * @ngdoc module * @name material.components.colors * * @description * Define $mdColors service and a `md-colors=""` attribute directive */ angular .module('material.components.colors', ['material.core']) .directive('mdColors', MdColorsDirective) .service('$mdColors', MdColorsService); /** * @ngdoc service * @name $mdColors * @module material.components.colors * * @description * With only defining themes, one couldn't get non AngularJS Material elements colored with Material colors, * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply * those values to element as CSS property values. * * @usage * * angular.controller('myCtrl', function ($mdColors) { * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); * ... * }); * * */ function MdColorsService($mdTheming, $mdUtil, $log) { colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES); // Publish service instance return { applyThemeColors: applyThemeColors, getThemeColor: getThemeColor, hasTheme: hasTheme }; // ******************************************** // Internal Methods // ******************************************** /** * @ngdoc method * @name $mdColors#applyThemeColors * * @description * Gets a color json object, keys are css properties and values are string of the wanted color * Then calculate the rgba() values based on the theme color parts * * @param {DOMElement} element the element to apply the styles on. * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color, * for example: `{color: 'red-A200-0.3'}`. * * @usage * * app.directive('myDirective', function($mdColors) { * return { * ... * link: function (scope, elem) { * $mdColors.applyThemeColors(elem, {color: 'red'}); * } * } * }); * */ function applyThemeColors(element, colorExpression) { try { if (colorExpression) { // Assign the calculate RGBA color values directly as inline CSS element.css(interpolateColors(colorExpression)); } } catch (e) { $log.error(e.message); } } /** * @ngdoc method * @name $mdColors#getThemeColor * * @description * Get parsed color from expression * * @param {string} expression string of a color expression (for instance `'red-700-0.8'`) * * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`) * * @usage * * angular.controller('myCtrl', function ($mdColors) { * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); * ... * }); * */ function getThemeColor(expression) { var color = extractColorOptions(expression); return parseColor(color); } /** * Return the parsed color * @param color hashmap of color definitions * @param contrast whether use contrast color for foreground * @returns rgba color string */ function parseColor(color, contrast) { contrast = contrast || false; var rgbValues = $mdTheming.PALETTES[color.palette][color.hue]; rgbValues = contrast ? rgbValues.contrast : rgbValues.value; return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})', [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity] ); } /** * Convert the color expression into an object with scope-interpolated values * Then calculate the rgba() values based on the theme color parts * * @results Hashmap of CSS properties with associated `rgba( )` string vales * * */ function interpolateColors(themeColors) { var rgbColors = {}; var hasColorProperty = themeColors.hasOwnProperty('color'); angular.forEach(themeColors, function (value, key) { var color = extractColorOptions(value); var hasBackground = key.indexOf('background') > -1; rgbColors[key] = parseColor(color); if (hasBackground && !hasColorProperty) { rgbColors.color = parseColor(color, true); } }); return rgbColors; } /** * Check if expression has defined theme * e.g. * 'myTheme-primary' => true * 'red-800' => false */ function hasTheme(expression) { return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]); } /** * For the evaluated expression, extract the color parts into a hash map */ function extractColorOptions(expression) { var parts = expression.split('-'); var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]); var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme(); return { theme: theme, palette: extractPalette(parts, theme), hue: extractHue(parts, theme), opacity: parts[2] || 1 }; } /** * Calculate the theme palette name */ function extractPalette(parts, theme) { // If the next section is one of the palettes we assume it's a two word palette // Two word palette can be also written in camelCase, forming camelCase to dash-case var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1; var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1); if (colorPalettes.indexOf(palette) === -1) { // If the palette is not in the palette list it's one of primary/accent/warn/background var scheme = $mdTheming.THEMES[theme].colors[palette]; if (!scheme) { throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette})); } palette = scheme.name; } return palette; } function extractHue(parts, theme) { var themeColors = $mdTheming.THEMES[theme].colors; if (parts[1] === 'hue') { var hueNumber = parseInt(parts.splice(2, 1)[0], 10); if (hueNumber < 1 || hueNumber > 3) { throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber})); } parts[1] = 'hue-' + hueNumber; if (!(parts[0] in themeColors)) { throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', { availableThemes: Object.keys(themeColors).join(', '), usedTheme: parts[0] })); } return themeColors[parts[0]].hues[parts[1]]; } return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default']; } } /** * @ngdoc directive * @name mdColors * @module material.components.colors * * @restrict A * * @description * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values. * * The format will be similar to our color defining in the scss files: * * ## `[?theme]-[palette]-[?hue]-[?opacity]` * - [theme] - default value is the default theme * - [palette] - can be either palette name or primary/accent/warn/background * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background) * - [opacity] - default is 1 * * > `?` indicates optional parameter * * @usage * *
*
* Color demo *
*
*
* * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression * to indicate a one-time data binding. * * * * * */ function MdColorsDirective($mdColors, $mdUtil, $log, $parse) { return { restrict: 'A', require: ['^?mdTheme'], compile: function (tElem, tAttrs) { var shouldWatch = shouldColorsWatch(); return function (scope, element, attrs, ctrl) { var mdThemeController = ctrl[0]; var lastColors = {}; var parseColors = function (theme) { if (typeof theme !== 'string') { theme = ''; } if (!attrs.mdColors) { attrs.mdColors = '{}'; } /** * Json.parse() does not work because the keys are not quoted; * use $parse to convert to a hash map */ var colors = $parse(attrs.mdColors)(scope); /** * If mdTheme is defined up the DOM tree * we add mdTheme theme to colors who doesn't specified a theme * * # example * *
*
* Color demo *
*
*
* * 'primary-600' will be 'myTheme-primary-600', * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix */ if (mdThemeController) { Object.keys(colors).forEach(function (prop) { var color = colors[prop]; if (!$mdColors.hasTheme(color)) { colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color; } }); } cleanElement(colors); return colors; }; var cleanElement = function (colors) { if (!angular.equals(colors, lastColors)) { var keys = Object.keys(lastColors); if (lastColors.background && !keys.color) { keys.push('color'); } keys.forEach(function (key) { element.css(key, ''); }); } lastColors = colors; }; /** * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes */ var unregisterChanges = angular.noop; if (mdThemeController) { unregisterChanges = mdThemeController.registerChanges(function (theme) { $mdColors.applyThemeColors(element, parseColors(theme)); }); } scope.$on('$destroy', function () { unregisterChanges(); }); try { if (shouldWatch) { scope.$watch(parseColors, angular.bind(this, $mdColors.applyThemeColors, element ), true); } else { $mdColors.applyThemeColors(element, parseColors()); } } catch (e) { $log.error(e.message); } }; function shouldColorsWatch() { // Simulate 1x binding and mark mdColorsWatch == false var rawColorExpression = tAttrs.mdColors; var bindOnce = rawColorExpression.indexOf('::') > -1; var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors); // Remove it for the postLink... tAttrs.mdColors = rawColorExpression.replace('::', ''); var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch); return (bindOnce || isStatic) ? false : hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true; } } }; } })(); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.content * * @description * Scrollable content */ mdContentDirective.$inject = ["$mdTheming"]; angular.module('material.components.content', [ 'material.core' ]) .directive('mdContent', mdContentDirective); /** * @ngdoc directive * @name mdContent * @module material.components.content * * @restrict E * * @description * * The `` directive is a container element useful for scrollable content. It achieves * this by setting the CSS `overflow` property to `auto` so that content can properly scroll. * * In general, `` components are not designed to be nested inside one another. If * possible, it is better to make them siblings. This often results in a better user experience as * having nested scrollbars may confuse the user. * * ## Troubleshooting * * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling * SVG icons and some other components. * * Additionally, we now also offer the `md-no-flicker` class which can be applied to any element * and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements * inside (which eliminates the flicker on iOS devices). * * _Note: Forcing an element to render on the GPU can have unintended side-effects, especially * related to the z-index of elements. Please use with caution and only on the elements needed._ * * @usage * * Add the `[layout-padding]` attribute to make the content padded. * * * * Lorem ipsum dolor sit amet, ne quod novum mei. * * */ function mdContentDirective($mdTheming) { return { restrict: 'E', controller: ['$scope', '$element', ContentController], link: function(scope, element) { element.addClass('_md'); // private md component indicator for styling $mdTheming(element); scope.$broadcast('$mdContentLoaded', element); iosScrollFix(element[0]); } }; function ContentController($scope, $element) { this.$scope = $scope; this.$element = $element; } } function iosScrollFix(node) { // IOS FIX: // If we scroll where there is no more room for the webview to scroll, // by default the webview itself will scroll up and down, this looks really // bad. So if we are scrolling to the very top or bottom, add/subtract one angular.element(node).on('$md.pressdown', function(ev) { // Only touch events if (ev.pointer.type !== 't') return; // Don't let a child content's touchstart ruin it for us. if (ev.$materialScrollFixed) return; ev.$materialScrollFixed = true; if (node.scrollTop === 0) { node.scrollTop = 1; } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { node.scrollTop -= 1; } }); } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.datepicker * @description Module for the datepicker component. */ angular.module('material.components.datepicker', [ 'material.core', 'material.components.icon', 'material.components.virtualRepeat' ]); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.dialog */ MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; MdDialogProvider.$inject = ["$$interimElementProvider"]; angular .module('material.components.dialog', [ 'material.core', 'material.components.backdrop' ]) .directive('mdDialog', MdDialogDirective) .provider('$mdDialog', MdDialogProvider); /** * @ngdoc directive * @name mdDialog * @module material.components.dialog * * @restrict E * * @description * `` - The dialog's template must be inside this element. * * Inside, use an `` element for the dialog's content, and use * an `` element for the dialog's actions. * * ## CSS * - `.md-dialog-content` - class that sets the padding on the content as the spec file * * ## Notes * - If you specify an `id` for the ``, the `` will have the same `id` * prefixed with `dialogContent_`. * * @usage * ### Dialog template * * * * * *

Number {{item}}

*
*
*
* * Close Dialog * *
*
*/ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { return { restrict: 'E', link: function(scope, element) { element.addClass('_md'); // private md component indicator for styling $mdTheming(element); $$rAF(function() { var images; var content = element[0].querySelector('md-dialog-content'); if (content) { images = content.getElementsByTagName('img'); addOverflowClass(); //-- delayed image loading may impact scroll height, check after images are loaded angular.element(images).on('load', addOverflowClass); } scope.$on('$destroy', function() { $mdDialog.destroy(element); }); /** * */ function addOverflowClass() { element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); } }); } }; } /** * @ngdoc service * @name $mdDialog * @module material.components.dialog * * @description * `$mdDialog` opens a dialog over the app to inform users about critical information or require * them to make decisions. There are two approaches for setup: a simple promise API * and regular object syntax. * * ## Restrictions * * - The dialog is always given an isolate scope. * - The dialog's template must have an outer `` element. * Inside, use an `` element for the dialog's content, and use * an `` element for the dialog's actions. * - Dialogs must cover the entire application to keep interactions inside of them. * Use the `parent` option to change where dialogs are appended. * * ## Sizing * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. * - Default max-width is 80% of the `rootElement` or `parent`. * * ## CSS * - `.md-dialog-content` - class that sets the padding on the content as the spec file * * @usage * *
*
* * Employee Alert! * *
*
* * Custom Dialog * *
*
* * Close Alert * *
*
* * Greet Employee * *
*
*
* * ### JavaScript: object syntax * * (function(angular, undefined){ * "use strict"; * * angular * .module('demoApp', ['ngMaterial']) * .controller('AppCtrl', AppController); * * function AppController($scope, $mdDialog) { * var alert; * $scope.showAlert = showAlert; * $scope.showDialog = showDialog; * $scope.items = [1, 2, 3]; * * // Internal method * function showAlert() { * alert = $mdDialog.alert({ * title: 'Attention', * textContent: 'This is an example of how easy dialogs can be!', * ok: 'Close' * }); * * $mdDialog * .show( alert ) * .finally(function() { * alert = undefined; * }); * } * * function showDialog($event) { * var parentEl = angular.element(document.body); * $mdDialog.show({ * parent: parentEl, * targetEvent: $event, * template: * '' + * ' '+ * ' '+ * ' '+ * '

Number {{item}}

' + * ' '+ * '
'+ * '
' + * ' ' + * ' ' + * ' Close Dialog' + * ' ' + * ' ' + * '
', * locals: { * items: $scope.items * }, * controller: DialogController * }); * function DialogController($scope, $mdDialog, items) { * $scope.items = items; * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * } * } * })(angular); *
* * ### Multiple Dialogs * Using the `multiple` option for the `$mdDialog` service allows developers to show multiple dialogs * at the same time. * * * // From plain options * $mdDialog.show({ * multiple: true * }); * * // From a dialog preset * $mdDialog.show( * $mdDialog * .alert() * .multiple(true) * ); * * * * ### Pre-Rendered Dialogs * By using the `contentElement` option, it is possible to use an already existing element in the DOM. * * > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new controller.
* > You can manually link the elements to a scope or instantiate a controller from the template (`ng-controller`) * * * $scope.showPrerenderedDialog = function() { * $mdDialog.show({ * contentElement: '#myStaticDialog', * parent: angular.element(document.body) * }); * }; * * * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector. * * *
*
* * This is a pre-rendered dialog. * *
*
*
* * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog * will not show up. * * It also possible to use a DOM element for the `contentElement` option. * - `contentElement: document.querySelector('#myStaticDialog')` * - `contentElement: angular.element(TEMPLATE)` * * When using a `template` as content element, it will be not compiled upon open. * This allows you to compile the element yourself and use it each time the dialog opens. * * ### Custom Presets * Developers are also able to create their own preset, which can be easily used without repeating * their options each time. * * * $mdDialogProvider.addPreset('testPreset', { * options: function() { * return { * template: * '' + * 'This is a custom preset' + * '', * controllerAs: 'dialog', * bindToController: true, * clickOutsideToClose: true, * escapeToClose: true * }; * } * }); * * * After you created your preset at config phase, you can easily access it. * * * $mdDialog.show( * $mdDialog.testPreset() * ); * * * ### JavaScript: promise API syntax, custom dialog template * * (function(angular, undefined){ * "use strict"; * * angular * .module('demoApp', ['ngMaterial']) * .controller('EmployeeController', EmployeeEditor) * .controller('GreetingController', GreetingController); * * // Fictitious Employee Editor to show how to use simple and complex dialogs. * * function EmployeeEditor($scope, $mdDialog) { * var alert; * * $scope.showAlert = showAlert; * $scope.closeAlert = closeAlert; * $scope.showGreeting = showCustomGreeting; * * $scope.hasAlert = function() { return !!alert }; * $scope.userName = $scope.userName || 'Bobby'; * * // Dialog #1 - Show simple alert dialog and cache * // reference to dialog instance * * function showAlert() { * alert = $mdDialog.alert() * .title('Attention, ' + $scope.userName) * .textContent('This is an example of how easy dialogs can be!') * .ok('Close'); * * $mdDialog * .show( alert ) * .finally(function() { * alert = undefined; * }); * } * * // Close the specified dialog instance and resolve with 'finished' flag * // Normally this is not needed, just use '$mdDialog.hide()' to close * // the most recent dialog popup. * * function closeAlert() { * $mdDialog.hide( alert, "finished" ); * alert = undefined; * } * * // Dialog #2 - Demonstrate more complex dialogs construction and popup. * * function showCustomGreeting($event) { * $mdDialog.show({ * targetEvent: $event, * template: * '' + * * ' Hello {{ employee }}!' + * * ' ' + * ' ' + * ' Close Greeting' + * ' ' + * ' ' + * '', * controller: 'GreetingController', * onComplete: afterShowAnimation, * locals: { employee: $scope.userName } * }); * * // When the 'enter' animation finishes... * * function afterShowAnimation(scope, element, options) { * // post-show code here: DOM element focus, etc. * } * } * * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog * // Here we used ng-controller="GreetingController as vm" and * // $scope.vm === * * function showCustomGreeting() { * * $mdDialog.show({ * clickOutsideToClose: true, * * scope: $scope, // use parent scope in template * preserveScope: true, // do not forget this if use parent scope * // Since GreetingController is instantiated with ControllerAs syntax * // AND we are passing the parent '$scope' to the dialog, we MUST * // use 'vm.' in the template markup * * template: '' + * ' ' + * ' Hi There {{vm.employee}}' + * ' ' + * '', * * controller: function DialogController($scope, $mdDialog) { * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * }); * } * * } * * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog * * function GreetingController($scope, $mdDialog, employee) { * // Assigned from construction locals options... * $scope.employee = employee; * * $scope.closeDialog = function() { * // Easily hides most recent dialog shown... * // no specific instance reference is needed. * $mdDialog.hide(); * }; * } * * })(angular); * */ /** * @ngdoc method * @name $mdDialog#alert * * @description * Builds a preconfigured dialog with the specified message. * * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: * * - $mdDialogPreset#title(string) - Sets the alert title. * - $mdDialogPreset#textContent(string) - Sets the alert message. * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize * module to be loaded. HTML is not run through Angular's compiler. * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * */ /** * @ngdoc method * @name $mdDialog#confirm * * @description * Builds a preconfigured dialog with the specified message. You can call show and the promise returned * will be resolved only if the user clicks the confirm action on the dialog. * * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: * * Additionally, it supports the following methods: * * - $mdDialogPreset#title(string) - Sets the confirm title. * - $mdDialogPreset#textContent(string) - Sets the confirm message. * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize * module to be loaded. HTML is not run through Angular's compiler. * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * */ /** * @ngdoc method * @name $mdDialog#prompt * * @description * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. * * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: * * Additionally, it supports the following methods: * * - $mdDialogPreset#title(string) - Sets the prompt title. * - $mdDialogPreset#textContent(string) - Sets the prompt message. * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize * module to be loaded. HTML is not run through Angular's compiler. * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input. * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * */ /** * @ngdoc method * @name $mdDialog#show * * @description * Show a dialog with the specified options. * * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and * `confirm()`, or an options object with the following properties: * - `templateUrl` - `{string=}`: The url of a template that will be used as the content * of the dialog. * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce). * This template should **never** be constructed with any kind of user input or user data. * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a * dialog opens, you can also use a DOM element.
* * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into * the dialog and restores it at the old DOM position upon close. * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM. * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a * `` tag if one is not provided. Defaults to true. Can be disabled if you provide a * custom dialog directive. * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object * that is used to determine the bounds (top, left, height, width) from which the Dialog will * originate. * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object * that is used to determine the bounds (top, left, height, width) to which the Dialog will * target. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, * it will create a new isolate scope. * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. * Default true. * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. * Default true. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to * close it. Default false. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. * Default true. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if * focusing some other way, as focus management is required for dialogs to be accessible. * Defaults to true. * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names * of values to inject into the controller. For example, `locals: {three: 3}` would inject * `three` into the controller, with the value 3. If `bindToController` is true, they will be * copied to the controller instead. * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending * to the root element of the application. * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is * starting. * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is * finished. * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the * close/hide() action is starting. This allows developers to run custom animations * in parallel the close animations. * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen * or not. Defaults to `false`. * - `multiple` `{boolean=}`: An option to allow this dialog to display over one that's currently open. * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or * rejected with `$mdDialog.cancel()`. */ /** * @ngdoc method * @name $mdDialog#hide * * @description * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. * * @param {*=} response An argument for the resolved promise. * * @returns {promise} A promise that is resolved when the dialog has been closed. */ /** * @ngdoc method * @name $mdDialog#cancel * * @description * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. * * @param {*=} response An argument for the rejected promise. * * @returns {promise} A promise that is resolved when the dialog has been closed. */ function MdDialogProvider($$interimElementProvider) { // Elements to capture and redirect focus when the user presses tab at the dialog boundary. MdDialogController.$inject = ["$mdDialog", "$mdConstant"]; dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"]; var topFocusTrap, bottomFocusTrap; return $$interimElementProvider('$mdDialog') .setDefaults({ methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'], options: dialogDefaultOptions }) .addPreset('alert', { methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', 'css'], options: advancedDialogOptions }) .addPreset('confirm', { methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', 'theme', 'css'], options: advancedDialogOptions }) .addPreset('prompt', { methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel', 'ok', 'cancel', 'theme', 'css', 'required'], options: advancedDialogOptions }); /* @ngInject */ function advancedDialogOptions() { return { template: [ '', ' ', '

{{ dialog.title }}

', '
', '
', '

{{::dialog.mdTextContent}}

', '
', ' ', ' ', ' ', '
', ' ', ' ', ' {{ dialog.cancel }}', ' ', ' ', ' {{ dialog.ok }}', ' ', ' ', '
' ].join('').replace(/\s\s+/g, ''), controller: MdDialogController, controllerAs: 'dialog', bindToController: true, }; } /** * Controller for the md-dialog interim elements * @ngInject */ function MdDialogController($mdDialog, $mdConstant) { // For compatibility with AngularJS 1.6+, we should always use the $onInit hook in // interimElements. The $mdCompiler simulates the $onInit hook for all versions. this.$onInit = function() { var isPrompt = this.$type == 'prompt'; if (isPrompt && this.initialValue) { this.result = this.initialValue; } this.hide = function() { $mdDialog.hide(isPrompt ? this.result : true); }; this.abort = function() { $mdDialog.cancel(); }; this.keypress = function($event) { if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { $mdDialog.hide(this.result); } }; }; } /* @ngInject */ function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, $log, $injector, $mdTheming, $interpolate, $mdInteraction) { return { hasBackdrop: true, isolateScope: true, onCompiling: beforeCompile, onShow: onShow, onShowing: beforeShow, onRemove: onRemove, clickOutsideToClose: false, escapeToClose: true, targetEvent: null, closeTo: null, openFrom: null, focusOnOpen: true, disableParentScroll: true, autoWrap: true, fullscreen: false, transformTemplate: function(template, options) { // Make the dialog container focusable, because otherwise the focus will be always redirected to // an element outside of the container, and the focus trap won't work probably.. // Also the tabindex is needed for the `escapeToClose` functionality, because // the keyDown event can't be triggered when the focus is outside of the container. var startSymbol = $interpolate.startSymbol(); var endSymbol = $interpolate.endSymbol(); var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol; return '
' + validatedTemplate(template) + '
'; /** * The specified template should contain a wrapper element.... */ function validatedTemplate(template) { if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { return '' + (template || '') + ''; } else { return template || ''; } } } }; function beforeCompile(options) { // Automatically apply the theme, if the user didn't specify a theme explicitly. // Those option changes need to be done, before the compilation has started, because otherwise // the option changes will be not available in the $mdCompilers locales. options.defaultTheme = $mdTheming.defaultTheme(); detectTheming(options); } function beforeShow(scope, element, options, controller) { if (controller) { var mdHtmlContent = controller.htmlContent || options.htmlContent || ''; var mdTextContent = controller.textContent || options.textContent || controller.content || options.content || ''; if (mdHtmlContent && !$injector.has('$sanitize')) { throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); } if (mdHtmlContent && mdTextContent) { throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); } // Only assign the content if nothing throws, otherwise it'll still be compiled. controller.mdHtmlContent = mdHtmlContent; controller.mdTextContent = mdTextContent; } } /** Show method for dialogs */ function onShow(scope, element, options, controller) { angular.element($document[0].body).addClass('md-dialog-is-showing'); var dialogElement = element.find('md-dialog'); // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly. // This is a very common problem, so we have to notify the developer about this. if (dialogElement.hasClass('ng-cloak')) { var message = '$mdDialog: using `` will affect the dialog opening animations.'; $log.warn( message, element[0] ); } captureParentAndFromToElements(options); configureAria(dialogElement, options); showBackdrop(scope, element, options); activateListeners(element, options); return dialogPopIn(element, options) .then(function() { lockScreenReader(element, options); warnDeprecatedActions(); focusOnOpen(); }); /** * Check to see if they used the deprecated .md-actions class and log a warning */ function warnDeprecatedActions() { if (element[0].querySelector('.md-actions')) { $log.warn('Using a class of md-actions is deprecated, please use .'); } } /** * For alerts, focus on content... otherwise focus on * the close button (or equivalent) */ function focusOnOpen() { if (options.focusOnOpen) { var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement; target.focus(); } /** * If no element with class dialog-close, try to find the last * button child in md-actions and assume it is a close button. * * If we find no actions at all, log a warning to the console. */ function findCloseButton() { return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child'); } } } /** * Remove function for all dialogs */ function onRemove(scope, element, options) { options.deactivateListeners(); options.unlockScreenReader(); options.hideBackdrop(options.$destroy); // Remove the focus traps that we added earlier for keeping focus within the dialog. if (topFocusTrap && topFocusTrap.parentNode) { topFocusTrap.parentNode.removeChild(topFocusTrap); } if (bottomFocusTrap && bottomFocusTrap.parentNode) { bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); } // For navigation $destroy events, do a quick, non-animated removal, // but for normal closes (from clicks, etc) animate the removal return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); /** * For normal closes, animate the removal. * For forced closes (like $destroy events), skip the animations */ function animateRemoval() { return dialogPopOut(element, options); } /** * Detach the element */ function detachAndClean() { angular.element($document[0].body).removeClass('md-dialog-is-showing'); // Reverse the container stretch if using a content element. if (options.contentElement) { options.reverseContainerStretch(); } // Exposed cleanup function from the $mdCompiler. options.cleanupElement(); // Restores the focus to the origin element if the last interaction upon opening was a keyboard. if (!options.$destroy && options.originInteraction === 'keyboard') { options.origin.focus(); } } } function detectTheming(options) { // Once the user specifies a targetEvent, we will automatically try to find the correct // nested theme. var targetEl; if (options.targetEvent && options.targetEvent.target) { targetEl = angular.element(options.targetEvent.target); } var themeCtrl = targetEl && targetEl.controller('mdTheme'); if (!themeCtrl) { return; } options.themeWatch = themeCtrl.$shouldWatch; var theme = options.theme || themeCtrl.$mdTheme; if (theme) { options.scope.theme = theme; } var unwatch = themeCtrl.registerChanges(function (newTheme) { options.scope.theme = newTheme; if (!options.themeWatch) { unwatch(); } }); } /** * Capture originator/trigger/from/to element information (if available) * and the parent container for the dialog; defaults to the $rootElement * unless overridden in the options.parent */ function captureParentAndFromToElements(options) { options.origin = angular.extend({ element: null, bounds: null, focus: angular.noop }, options.origin || {}); options.parent = getDomElement(options.parent, $rootElement); options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); if ( options.targetEvent ) { options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); options.originInteraction = $mdInteraction.getLastInteractionType(); } /** * Identify the bounding RECT for the target element * */ function getBoundingClientRect (element, orig) { var source = angular.element((element || {})); if (source && source.length) { // Compute and save the target element's bounding rect, so that if the // element is hidden when the dialog closes, we can shrink the dialog // back to the same position it expanded from. // // Checking if the source is a rect object or a DOM element var bounds = {top:0,left:0,height:0,width:0}; var hasFn = angular.isFunction(source[0].getBoundingClientRect); return angular.extend(orig || {}, { element : hasFn ? source : undefined, bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), focus : angular.bind(source, source.focus), }); } } /** * If the specifier is a simple string selector, then query for * the DOM element. */ function getDomElement(element, defaultElement) { if (angular.isString(element)) { element = $document[0].querySelector(element); } // If we have a reference to a raw dom element, always wrap it in jqLite return angular.element(element || defaultElement); } } /** * Listen for escape keys and outside clicks to auto close */ function activateListeners(element, options) { var window = angular.element($window); var onWindowResize = $mdUtil.debounce(function() { stretchDialogContainerToViewport(element, options); }, 60); var removeListeners = []; var smartClose = function() { // Only 'confirm' dialogs have a cancel button... escape/clickOutside will // cancel or fallback to hide. var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; $mdUtil.nextTick(closeFn, true); }; if (options.escapeToClose) { var parentTarget = options.parent; var keyHandlerFn = function(ev) { if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { ev.stopPropagation(); ev.preventDefault(); smartClose(); } }; // Add keydown listeners element.on('keydown', keyHandlerFn); parentTarget.on('keydown', keyHandlerFn); // Queue remove listeners function removeListeners.push(function() { element.off('keydown', keyHandlerFn); parentTarget.off('keydown', keyHandlerFn); }); } // Register listener to update dialog on window resize window.on('resize', onWindowResize); removeListeners.push(function() { window.off('resize', onWindowResize); }); if (options.clickOutsideToClose) { var target = element; var sourceElem; // Keep track of the element on which the mouse originally went down // so that we can only close the backdrop when the 'click' started on it. // A simple 'click' handler does not work, // it sets the target object as the element the mouse went down on. var mousedownHandler = function(ev) { sourceElem = ev.target; }; // We check if our original element and the target is the backdrop // because if the original was the backdrop and the target was inside the dialog // we don't want to dialog to close. var mouseupHandler = function(ev) { if (sourceElem === target[0] && ev.target === target[0]) { ev.stopPropagation(); ev.preventDefault(); smartClose(); } }; // Add listeners target.on('mousedown', mousedownHandler); target.on('mouseup', mouseupHandler); // Queue remove listeners function removeListeners.push(function() { target.off('mousedown', mousedownHandler); target.off('mouseup', mouseupHandler); }); } // Attach specific `remove` listener handler options.deactivateListeners = function() { removeListeners.forEach(function(removeFn) { removeFn(); }); options.deactivateListeners = null; }; } /** * Show modal backdrop element... */ function showBackdrop(scope, element, options) { if (options.disableParentScroll) { // !! DO this before creating the backdrop; since disableScrollAround() // configures the scroll offset; which is used by mdBackDrop postLink() options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); } if (options.hasBackdrop) { options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque"); $animate.enter(options.backdrop, options.parent); } /** * Hide modal backdrop element... */ options.hideBackdrop = function hideBackdrop($destroy) { if (options.backdrop) { if ( !!$destroy ) options.backdrop.remove(); else $animate.leave(options.backdrop); } if (options.disableParentScroll) { options.restoreScroll && options.restoreScroll(); delete options.restoreScroll; } options.hideBackdrop = null; }; } /** * Inject ARIA-specific attributes appropriate for Dialogs */ function configureAria(element, options) { var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; var dialogContent = element.find('md-dialog-content'); var existingDialogId = element.attr('id'); var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid()); element.attr({ 'role': role, 'tabIndex': '-1' }); if (dialogContent.length === 0) { dialogContent = element; // If the dialog element already had an ID, don't clobber it. if (existingDialogId) { dialogContentId = existingDialogId; } } dialogContent.attr('id', dialogContentId); element.attr('aria-describedby', dialogContentId); if (options.ariaLabel) { $mdAria.expect(element, 'aria-label', options.ariaLabel); } else { $mdAria.expectAsync(element, 'aria-label', function() { // If dialog title is specified, set aria-label with it // See https://github.com/angular/material/issues/10582 if (options.title) { return options.title; } else { var words = dialogContent.text().split(/\s+/); if (words.length > 3) words = words.slice(0, 3).concat('...'); return words.join(' '); } }); } // Set up elements before and after the dialog content to capture focus and // redirect back into the dialog. topFocusTrap = document.createElement('div'); topFocusTrap.classList.add('md-dialog-focus-trap'); topFocusTrap.tabIndex = 0; bottomFocusTrap = topFocusTrap.cloneNode(false); // When focus is about to move out of the dialog, we want to intercept it and redirect it // back to the dialog element. var focusHandler = function() { element.focus(); }; topFocusTrap.addEventListener('focus', focusHandler); bottomFocusTrap.addEventListener('focus', focusHandler); // The top focus trap inserted immeidately before the md-dialog element (as a sibling). // The bottom focus trap is inserted at the very end of the md-dialog element (as a child). element[0].parentNode.insertBefore(topFocusTrap, element[0]); element.after(bottomFocusTrap); } /** * Prevents screen reader interaction behind modal window * on swipe interfaces */ function lockScreenReader(element, options) { var isHidden = true; // get raw DOM node walkDOM(element[0]); options.unlockScreenReader = function() { isHidden = false; walkDOM(element[0]); options.unlockScreenReader = null; }; /** * Walk DOM to apply or remove aria-hidden on sibling nodes * and parent sibling nodes * */ function walkDOM(element) { while (element.parentNode) { if (element === document.body) { return; } var children = element.parentNode.children; for (var i = 0; i < children.length; i++) { // skip over child if it is an ascendant of the dialog // or a script or style tag if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE']) && !children[i].hasAttribute('aria-live')) { children[i].setAttribute('aria-hidden', isHidden); } } walkDOM(element = element.parentNode); } } } /** * Ensure the dialog container fill-stretches to the viewport */ function stretchDialogContainerToViewport(container, options) { var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; var previousStyles = { top: container.css('top'), height: container.css('height') }; // If the body is fixed, determine the distance to the viewport in relative from the parent. var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top); container.css({ top: (isFixed ? parentTop : 0) + 'px', height: height ? height + 'px' : '100%' }); return function() { // Reverts the modified styles back to the previous values. // This is needed for contentElements, which should have the same styles after close // as before. container.css(previousStyles); }; } /** * Dialog open and pop-in animation */ function dialogPopIn(container, options) { // Add the `md-dialog-container` to the DOM options.parent.append(container); options.reverseContainerStretch = stretchDialogContainerToViewport(container, options); var dialogEl = container.find('md-dialog'); var animator = $mdUtil.dom.animator; var buildTranslateToOrigin = animator.calculateZoomToOrigin; var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'}; var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen); return animator .translate3d(dialogEl, from, to, translateOptions) .then(function(animateReversal) { // Build a reversal translate function synced to this translation... options.reverseAnimate = function() { delete options.reverseAnimate; if (options.closeTo) { // Using the opposite classes to create a close animation to the closeTo element translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'}; from = to; to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo)); return animator .translate3d(dialogEl, from, to,translateOptions); } return animateReversal( to = animator.toTransformCss( // in case the origin element has moved or is hidden, // let's recalculate the translateCSS buildTranslateToOrigin(dialogEl, options.origin) ) ); }; // Function to revert the generated animation styles on the dialog element. // Useful when using a contentElement instead of a template. options.clearAnimate = function() { delete options.clearAnimate; // Remove the transition classes, added from $animateCSS, since those can't be removed // by reversely running the animator. dialogEl.removeClass([ translateOptions.transitionOutClass, translateOptions.transitionInClass ].join(' ')); // Run the animation reversely to remove the previous added animation styles. return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {}); }; return true; }); } /** * Dialog close and pop-out animation */ function dialogPopOut(container, options) { return options.reverseAnimate().then(function() { if (options.contentElement) { // When we use a contentElement, we want the element to be the same as before. // That means, that we have to clear all the animation properties, like transform. options.clearAnimate(); } }); } /** * Utility function to filter out raw DOM nodes */ function isNodeOneOf(elem, nodeTypeArray) { if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { return true; } } } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.divider * @description Divider module! */ MdDividerDirective.$inject = ["$mdTheming"]; angular.module('material.components.divider', [ 'material.core' ]) .directive('mdDivider', MdDividerDirective); /** * @ngdoc directive * @name mdDivider * @module material.components.divider * @restrict E * * @description * Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content. * * @param {boolean=} md-inset Add this attribute to activate the inset divider style. * @usage * * * * * * */ function MdDividerDirective($mdTheming) { return { restrict: 'E', link: $mdTheming }; } })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc module * @name material.components.fabActions */ MdFabActionsDirective.$inject = ["$mdUtil"]; angular .module('material.components.fabActions', ['material.core']) .directive('mdFabActions', MdFabActionsDirective); /** * @ngdoc directive * @name mdFabActions * @module material.components.fabActions * * @restrict E * * @description * The `` directive is used inside of a `` or * `` directive to mark an element (or elements) as the actions and setup the * proper event listeners. * * @usage * See the `` or `` directives for example usage. */ function MdFabActionsDirective($mdUtil) { return { restrict: 'E', require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], compile: function(element, attributes) { var children = element.children(); var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat'); // Support both ng-repeat and static content if (hasNgRepeat) { children.addClass('md-fab-action-item'); } else { // Wrap every child in a new div and add a class that we can scale/fling independently children.wrap('
'); } } }; } })(); })(); (function(){ "use strict"; (function() { 'use strict'; MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; angular.module('material.components.fabShared', ['material.core']) .controller('MdFabController', MdFabController); function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { var vm = this; var initialAnimationAttempts = 0; // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops vm.open = function() { $scope.$evalAsync("vm.isOpen = true"); }; vm.close = function() { // Async eval to avoid conflicts with existing digest loops $scope.$evalAsync("vm.isOpen = false"); // Focus the trigger when the element closes so users can still tab to the next item $element.find('md-fab-trigger')[0].focus(); }; // Toggle the open/close state when the trigger is clicked vm.toggle = function() { $scope.$evalAsync("vm.isOpen = !vm.isOpen"); }; /* * AngularJS Lifecycle hook for newer AngularJS versions. * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. */ vm.$onInit = function() { setupDefaults(); setupListeners(); setupWatchers(); fireInitialAnimations(); }; // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, // manually call the $onInit hook. if (angular.version.major === 1 && angular.version.minor <= 4) { this.$onInit(); } function setupDefaults() { // Set the default direction to 'down' if none is specified vm.direction = vm.direction || 'down'; // Set the default to be closed vm.isOpen = vm.isOpen || false; // Start the keyboard interaction at the first action resetActionIndex(); // Add an animations waiting class so we know not to run $element.addClass('md-animations-waiting'); } function setupListeners() { var eventTypes = [ 'click', 'focusin', 'focusout' ]; // Add our listeners angular.forEach(eventTypes, function(eventType) { $element.on(eventType, parseEvents); }); // Remove our listeners when destroyed $scope.$on('$destroy', function() { angular.forEach(eventTypes, function(eventType) { $element.off(eventType, parseEvents); }); // remove any attached keyboard handlers in case element is removed while // speed dial is open disableKeyboard(); }); } var closeTimeout; function parseEvents(event) { // If the event is a click, just handle it if (event.type == 'click') { handleItemClick(event); } // If we focusout, set a timeout to close the element if (event.type == 'focusout' && !closeTimeout) { closeTimeout = $timeout(function() { vm.close(); }, 100, false); } // If we see a focusin and there is a timeout about to run, cancel it so we stay open if (event.type == 'focusin' && closeTimeout) { $timeout.cancel(closeTimeout); closeTimeout = null; } } function resetActionIndex() { vm.currentActionIndex = -1; } function setupWatchers() { // Watch for changes to the direction and update classes/attributes $scope.$watch('vm.direction', function(newDir, oldDir) { // Add the appropriate classes so we can target the direction in the CSS $animate.removeClass($element, 'md-' + oldDir); $animate.addClass($element, 'md-' + newDir); // Reset the action index since it may have changed resetActionIndex(); }); var trigger, actions; // Watch for changes to md-open $scope.$watch('vm.isOpen', function(isOpen) { // Reset the action index since it may have changed resetActionIndex(); // We can't get the trigger/actions outside of the watch because the component hasn't been // linked yet, so we wait until the first watch fires to cache them. if (!trigger || !actions) { trigger = getTriggerElement(); actions = getActionsElement(); } if (isOpen) { enableKeyboard(); } else { disableKeyboard(); } var toAdd = isOpen ? 'md-is-open' : ''; var toRemove = isOpen ? '' : 'md-is-open'; // Set the proper ARIA attributes trigger.attr('aria-haspopup', true); trigger.attr('aria-expanded', isOpen); actions.attr('aria-hidden', !isOpen); // Animate the CSS classes $animate.setClass($element, toAdd, toRemove); }); } function fireInitialAnimations() { // If the element is actually visible on the screen if ($element[0].scrollHeight > 0) { // Fire our animation $animate.addClass($element, '_md-animations-ready').then(function() { // Remove the waiting class $element.removeClass('md-animations-waiting'); }); } // Otherwise, try for up to 1 second before giving up else if (initialAnimationAttempts < 10) { $timeout(fireInitialAnimations, 100); // Increment our counter initialAnimationAttempts = initialAnimationAttempts + 1; } } function enableKeyboard() { $element.on('keydown', keyPressed); // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button) $mdUtil.nextTick(function() { angular.element(document).on('click touchend', checkForOutsideClick); }); // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but // this breaks accessibility, especially on mobile, since you have no arrow keys to press //resetActionTabIndexes(); } function disableKeyboard() { $element.off('keydown', keyPressed); angular.element(document).off('click touchend', checkForOutsideClick); } function checkForOutsideClick(event) { if (event.target) { var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); if (!closestTrigger && !closestActions) { vm.close(); } } } function keyPressed(event) { switch (event.which) { case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false; case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false; } } function doActionPrev(event) { focusAction(event, -1); } function doActionNext(event) { focusAction(event, 1); } function focusAction(event, direction) { var actions = resetActionTabIndexes(); // Increment/decrement the counter with restrictions vm.currentActionIndex = vm.currentActionIndex + direction; vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex); vm.currentActionIndex = Math.max(0, vm.currentActionIndex); // Focus the element var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; angular.element(focusElement).attr('tabindex', 0); focusElement.focus(); // Make sure the event doesn't bubble and cause something else event.preventDefault(); event.stopImmediatePropagation(); } function resetActionTabIndexes() { // Grab all of the actions var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); // Disable all other actions for tabbing angular.forEach(actions, function(action) { angular.element(angular.element(action).children()[0]).attr('tabindex', -1); }); return actions; } function doKeyLeft(event) { if (vm.direction === 'left') { doActionNext(event); } else { doActionPrev(event); } } function doKeyUp(event) { if (vm.direction === 'down') { doActionPrev(event); } else { doActionNext(event); } } function doKeyRight(event) { if (vm.direction === 'left') { doActionPrev(event); } else { doActionNext(event); } } function doKeyDown(event) { if (vm.direction === 'up') { doActionPrev(event); } else { doActionNext(event); } } function isTrigger(element) { return $mdUtil.getClosest(element, 'md-fab-trigger'); } function isAction(element) { return $mdUtil.getClosest(element, 'md-fab-actions'); } function handleItemClick(event) { if (isTrigger(event.target)) { vm.toggle(); } if (isAction(event.target)) { vm.close(); } } function getTriggerElement() { return $element.find('md-fab-trigger'); } function getActionsElement() { return $element.find('md-fab-actions'); } } })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * The duration of the CSS animation in milliseconds. * * @type {number} */ MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; var cssAnimationDuration = 300; /** * @ngdoc module * @name material.components.fabSpeedDial */ angular // Declare our module .module('material.components.fabSpeedDial', [ 'material.core', 'material.components.fabShared', 'material.components.fabActions' ]) // Register our directive .directive('mdFabSpeedDial', MdFabSpeedDialDirective) // Register our custom animations .animation('.md-fling', MdFabSpeedDialFlingAnimation) .animation('.md-scale', MdFabSpeedDialScaleAnimation) // Register a service for each animation so that we can easily inject them into unit tests .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); /** * @ngdoc directive * @name mdFabSpeedDial * @module material.components.fabSpeedDial * * @restrict E * * @description * The `` directive is used to present a series of popup elements (usually * ``s) for quick access to common actions. * * There are currently two animations available by applying one of the following classes to * the component: * * - `md-fling` - The speed dial items appear from underneath the trigger and move into their * appropriate positions. * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. * * You may also easily position the trigger by applying one one of the following classes to the * `` element: * - `md-fab-top-left` * - `md-fab-top-right` * - `md-fab-bottom-left` * - `md-fab-bottom-right` * * These CSS classes use `position: absolute`, so you need to ensure that the container element * also uses `position: absolute` or `position: relative` in order for them to work. * * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to * open or close the speed dial. However, if you wish to allow users to hover over the empty * space where the actions will appear, you must also add the `md-hover-full` class to the speed * dial element. Without this, the hover effect will only occur on top of the trigger. * * See the demos for more information. * * ## Troubleshooting * * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on * the parent container to ensure that it is only visible once ready. We have plans to remove this * necessity in the future. * * @usage * * * * * * * * * * * * * * * * * * * @param {string} md-direction From which direction you would like the speed dial to appear * relative to the trigger element. * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. */ function MdFabSpeedDialDirective() { return { restrict: 'E', scope: { direction: '@?mdDirection', isOpen: '=?mdOpen' }, bindToController: true, controller: 'MdFabController', controllerAs: 'vm', link: FabSpeedDialLink }; function FabSpeedDialLink(scope, element) { // Prepend an element to hold our CSS variables so we can use them in the animations below element.prepend('
'); } } function MdFabSpeedDialFlingAnimation($timeout) { function delayDone(done) { $timeout(done, cssAnimationDuration, false); } function runAnimation(element) { // Don't run if we are still waiting and we are not ready if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) { return; } var el = element[0]; var ctrl = element.controller('mdFabSpeedDial'); var items = el.querySelectorAll('.md-fab-action-item'); // Grab our trigger element var triggerElement = el.querySelector('md-fab-trigger'); // Grab our element which stores CSS variables var variablesElement = el.querySelector('._md-css-variables'); // Setup JS variables based on our CSS variables var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); // Always reset the items to their natural position/state angular.forEach(items, function(item, index) { var styles = item.style; styles.transform = styles.webkitTransform = ''; styles.transitionDelay = ''; styles.opacity = 1; // Make the items closest to the trigger have the highest z-index styles.zIndex = (items.length - index) + startZIndex; }); // Set the trigger to be above all of the actions so they disappear behind it. triggerElement.style.zIndex = startZIndex + items.length + 1; // If the control is closed, hide the items behind the trigger if (!ctrl.isOpen) { angular.forEach(items, function(item, index) { var newPosition, axis; var styles = item.style; // Make sure to account for differences in the dimensions of the trigger verses the items // so that we can properly center everything; this helps hide the item's shadows behind // the trigger. var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; switch (ctrl.direction) { case 'up': newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); axis = 'Y'; break; case 'down': newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); axis = 'Y'; break; case 'left': newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); axis = 'X'; break; case 'right': newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); axis = 'X'; break; } var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; styles.transform = styles.webkitTransform = newTranslate; }); } } return { addClass: function(element, className, done) { if (element.hasClass('md-fling')) { runAnimation(element); delayDone(done); } else { done(); } }, removeClass: function(element, className, done) { runAnimation(element); delayDone(done); } }; } function MdFabSpeedDialScaleAnimation($timeout) { function delayDone(done) { $timeout(done, cssAnimationDuration, false); } var delay = 65; function runAnimation(element) { var el = element[0]; var ctrl = element.controller('mdFabSpeedDial'); var items = el.querySelectorAll('.md-fab-action-item'); // Grab our element which stores CSS variables var variablesElement = el.querySelector('._md-css-variables'); // Setup JS variables based on our CSS variables var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); // Always reset the items to their natural position/state angular.forEach(items, function(item, index) { var styles = item.style, offsetDelay = index * delay; styles.opacity = ctrl.isOpen ? 1 : 0; styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; // Make the items closest to the trigger have the highest z-index styles.zIndex = (items.length - index) + startZIndex; }); } return { addClass: function(element, className, done) { runAnimation(element); delayDone(done); }, removeClass: function(element, className, done) { runAnimation(element); delayDone(done); } }; } })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc module * @name material.components.fabToolbar */ angular // Declare our module .module('material.components.fabToolbar', [ 'material.core', 'material.components.fabShared', 'material.components.fabActions' ]) // Register our directive .directive('mdFabToolbar', MdFabToolbarDirective) // Register our custom animations .animation('.md-fab-toolbar', MdFabToolbarAnimation) // Register a service for the animation so that we can easily inject it into unit tests .service('mdFabToolbarAnimation', MdFabToolbarAnimation); /** * @ngdoc directive * @name mdFabToolbar * @module material.components.fabToolbar * * @restrict E * * @description * * The `` directive is used to present a toolbar of elements (usually ``s) * for quick access to common actions when a floating action button is activated (via click or * keyboard navigation). * * You may also easily position the trigger by applying one one of the following classes to the * `` element: * - `md-fab-top-left` * - `md-fab-top-right` * - `md-fab-bottom-left` * - `md-fab-bottom-right` * * These CSS classes use `position: absolute`, so you need to ensure that the container element * also uses `position: absolute` or `position: relative` in order for them to work. * * @usage * * * * * * * * * * * * * * * * * * * * * * @param {string} md-direction From which direction you would like the toolbar items to appear * relative to the trigger element. Supports `left` and `right` directions. * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. */ function MdFabToolbarDirective() { return { restrict: 'E', transclude: true, template: '
' + '
' + '
', scope: { direction: '@?mdDirection', isOpen: '=?mdOpen' }, bindToController: true, controller: 'MdFabController', controllerAs: 'vm', link: link }; function link(scope, element, attributes) { // Add the base class for animations element.addClass('md-fab-toolbar'); // Prepend the background element to the trigger's button element.find('md-fab-trigger').find('button') .prepend('
'); } } function MdFabToolbarAnimation() { function runAnimation(element, className, done) { // If no className was specified, don't do anything if (!className) { return; } var el = element[0]; var ctrl = element.controller('mdFabToolbar'); // Grab the relevant child elements var backgroundElement = el.querySelector('.md-fab-toolbar-background'); var triggerElement = el.querySelector('md-fab-trigger button'); var toolbarElement = el.querySelector('md-toolbar'); var iconElement = el.querySelector('md-fab-trigger button md-icon'); var actions = element.find('md-fab-actions').children(); // If we have both elements, use them to position the new background if (triggerElement && backgroundElement) { // Get our variables var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); var width = el.offsetWidth; var height = el.offsetHeight; // Make it twice as big as it should be since we scale from the center var scale = 2 * (width / triggerElement.offsetWidth); // Set some basic styles no matter what animation we're doing backgroundElement.style.backgroundColor = color; backgroundElement.style.borderRadius = width + 'px'; // If we're open if (ctrl.isOpen) { // Turn on toolbar pointer events when closed toolbarElement.style.pointerEvents = 'inherit'; backgroundElement.style.width = triggerElement.offsetWidth + 'px'; backgroundElement.style.height = triggerElement.offsetHeight + 'px'; backgroundElement.style.transform = 'scale(' + scale + ')'; // Set the next close animation to have the proper delays backgroundElement.style.transitionDelay = '0ms'; iconElement && (iconElement.style.transitionDelay = '.3s'); // Apply a transition delay to actions angular.forEach(actions, function(action, index) { action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; }); } else { // Turn off toolbar pointer events when closed toolbarElement.style.pointerEvents = 'none'; // Scale it back down to the trigger's size backgroundElement.style.transform = 'scale(1)'; // Reset the position backgroundElement.style.top = '0'; if (element.hasClass('md-right')) { backgroundElement.style.left = '0'; backgroundElement.style.right = null; } if (element.hasClass('md-left')) { backgroundElement.style.right = '0'; backgroundElement.style.left = null; } // Set the next open animation to have the proper delays backgroundElement.style.transitionDelay = '200ms'; iconElement && (iconElement.style.transitionDelay = '0ms'); // Apply a transition delay to actions angular.forEach(actions, function(action, index) { action.style.transitionDelay = 200 + (index * 25) + 'ms'; }); } } } return { addClass: function(element, className, done) { runAnimation(element, className, done); done(); }, removeClass: function(element, className, done) { runAnimation(element, className, done); done(); } }; } })(); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.icon * @description * Icon */ angular.module('material.components.icon', ['material.core']); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.gridList */ GridListController.$inject = ["$mdUtil"]; GridLayoutFactory.$inject = ["$mdUtil"]; GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; GridTileDirective.$inject = ["$mdMedia"]; angular.module('material.components.gridList', ['material.core']) .directive('mdGridList', GridListDirective) .directive('mdGridTile', GridTileDirective) .directive('mdGridTileFooter', GridTileCaptionDirective) .directive('mdGridTileHeader', GridTileCaptionDirective) .factory('$mdGridLayout', GridLayoutFactory); /** * @ngdoc directive * @name mdGridList * @module material.components.gridList * @restrict E * @description * Grid lists are an alternative to standard list views. Grid lists are distinct * from grids used for layouts and other visual presentations. * * A grid list is best suited to presenting a homogenous data type, typically * images, and is optimized for visual comprehension and differentiating between * like data types. * * A grid list is a continuous element consisting of tessellated, regular * subdivisions called cells that contain tiles (`md-grid-tile`). * * Concept of grid explained visually * Grid concepts legend * * Cells are arrayed vertically and horizontally within the grid. * * Tiles hold content and can span one or more cells vertically or horizontally. * * ### Responsive Attributes * * The `md-grid-list` directive supports "responsive" attributes, which allow * different `md-cols`, `md-gutter` and `md-row-height` values depending on the * currently matching media query. * * In order to set a responsive attribute, first define the fallback value with * the standard attribute name, then add additional attributes with the * following convention: `{base-attribute-name}-{media-query-name}="{value}"` * (ie. `md-cols-lg="8"`) * * @param {number} md-cols Number of columns in the grid. * @param {string} md-row-height One of *
    *
  • CSS length - Fixed height rows (eg. `8px` or `1rem`)
  • *
  • `{width}:{height}` - Ratio of width to height (eg. * `md-row-height="16:9"`)
  • *
  • `"fit"` - Height will be determined by subdividing the available * height by the number of rows
  • *
* @param {string=} md-gutter The amount of space between tiles in CSS units * (default 1px) * @param {expression=} md-on-layout Expression to evaluate after layout. Event * object is available as `$event`, and contains performance information. * * @usage * Basic: * * * * * * * Fixed-height rows: * * * * * * * Fit rows: * * * * * * * Using responsive attributes: * * * * * */ function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { return { restrict: 'E', controller: GridListController, scope: { mdOnLayout: '&' }, link: postLink }; function postLink(scope, element, attrs, ctrl) { element.addClass('_md'); // private md component indicator for styling // Apply semantics element.attr('role', 'list'); // Provide the controller with a way to trigger layouts. ctrl.layoutDelegate = layoutDelegate; var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), unwatchAttrs = watchMedia(); scope.$on('$destroy', unwatchMedia); /** * Watches for changes in media, invalidating layout as necessary. */ function watchMedia() { for (var mediaName in $mdConstant.MEDIA) { $mdMedia(mediaName); // initialize $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) .addListener(invalidateLayout); } return $mdMedia.watchResponsiveAttributes( ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); } function unwatchMedia() { ctrl.layoutDelegate = angular.noop; unwatchAttrs(); for (var mediaName in $mdConstant.MEDIA) { $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) .removeListener(invalidateLayout); } } /** * Performs grid layout if the provided mediaName matches the currently * active media type. */ function layoutIfMediaMatch(mediaName) { if (mediaName == null) { // TODO(shyndman): It would be nice to only layout if we have // instances of attributes using this media type ctrl.invalidateLayout(); } else if ($mdMedia(mediaName)) { ctrl.invalidateLayout(); } } var lastLayoutProps; /** * Invokes the layout engine, and uses its results to lay out our * tile elements. * * @param {boolean} tilesInvalidated Whether tiles have been * added/removed/moved since the last layout. This is to avoid situations * where tiles are replaced with properties identical to their removed * counterparts. */ function layoutDelegate(tilesInvalidated) { var tiles = getTileElements(); var props = { tileSpans: getTileSpans(tiles), colCount: getColumnCount(), rowMode: getRowMode(), rowHeight: getRowHeight(), gutter: getGutter() }; if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { return; } var performance = $mdGridLayout(props.colCount, props.tileSpans, tiles) .map(function(tilePositions, rowCount) { return { grid: { element: element, style: getGridStyle(props.colCount, rowCount, props.gutter, props.rowMode, props.rowHeight) }, tiles: tilePositions.map(function(ps, i) { return { element: angular.element(tiles[i]), style: getTileStyle(ps.position, ps.spans, props.colCount, rowCount, props.gutter, props.rowMode, props.rowHeight) } }) } }) .reflow() .performance(); // Report layout scope.mdOnLayout({ $event: { performance: performance } }); lastLayoutProps = props; } // Use $interpolate to do some simple string interpolation as a convenience. var startSymbol = $interpolate.startSymbol(); var endSymbol = $interpolate.endSymbol(); // Returns an expression wrapped in the interpolator's start and end symbols. function expr(exprStr) { return startSymbol + exprStr + endSymbol; } // The amount of space a single 1x1 tile would take up (either width or height), used as // a basis for other calculations. This consists of taking the base size percent (as would be // if evenly dividing the size between cells), and then subtracting the size of one gutter. // However, since there are no gutters on the edges, each tile only uses a fration // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per // tile, and then breaking up the extra gutter on the edge evenly among the cells). var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. // The position comes the size of a 1x1 tile plus gutter for each previous tile in the // row/column (offset). var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back // in the space that the gutter would normally have used (which was already accounted for in // the base unit calculation). var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); /** * Gets the styles applied to a tile element described by the given parameters. * @param {{row: number, col: number}} position The row and column indices of the tile. * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. * @param {number} colCount The number of columns. * @param {number} rowCount The number of rows. * @param {string} gutter The amount of space between tiles. This will be something like * '5px' or '2em'. * @param {string} rowMode The row height mode. Can be one of: * 'fixed': all rows have a fixed size, given by rowHeight, * 'ratio': row height defined as a ratio to width, or * 'fit': fit to the grid-list element height, divinding evenly among rows. * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). * @returns {Object} Map of CSS properties to be applied to the style element. Will define * values for top, left, width, height, marginTop, and paddingTop. */ function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { // TODO(shyndman): There are style caching opportunities here. // Percent of the available horizontal space that one column takes up. var hShare = (1 / colCount) * 100; // Fraction of the gutter size that each column takes up. var hGutterShare = (colCount - 1) / colCount; // Base horizontal size of a column. var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); // The width and horizontal position of each tile is always calculated the same way, but the // height and vertical position depends on the rowMode. var ltr = document.dir != 'rtl' && document.body.dir != 'rtl'; var style = ltr ? { left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), // resets paddingTop: '', marginTop: '', top: '', height: '' } : { right: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), // resets paddingTop: '', marginTop: '', top: '', height: '' }; switch (rowMode) { case 'fixed': // In fixed mode, simply use the given rowHeight. style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); break; case 'ratio': // Percent of the available vertical space that one row takes up. Here, rowHeight holds // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. var vShare = hShare / rowHeight; // Base veritcal size of a row. var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); // padidngTop and marginTop are used to maintain the given aspect ratio, as // a percentage-based value for these properties is applied to the *width* of the // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); break; case 'fit': // Fraction of the gutter size that each column takes up. var vGutterShare = (rowCount - 1) / rowCount; // Percent of the available vertical space that one row takes up. var vShare = (1 / rowCount) * 100; // Base vertical size of a row. var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); break; } return style; } function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { var style = {}; switch(rowMode) { case 'fixed': style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); style.paddingBottom = ''; break; case 'ratio': // rowHeight is width / height var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, hShare = (1 / colCount) * 100, vShare = hShare * (1 / rowHeight), vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); style.height = ''; style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); break; case 'fit': // noop, as the height is user set break; } return style; } function getTileElements() { return [].filter.call(element.children(), function(ele) { return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; }); } /** * Gets an array of objects containing the rowspan and colspan for each tile. * @returns {Array<{row: number, col: number}>} */ function getTileSpans(tileElements) { return [].map.call(tileElements, function(ele) { var ctrl = angular.element(ele).controller('mdGridTile'); return { row: parseInt( $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, col: parseInt( $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 }; }); } function getColumnCount() { var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); if (isNaN(colCount)) { throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; } return colCount; } function getGutter() { return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); } function getRowHeight() { var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); if (!rowHeight) { throw 'md-grid-list: md-row-height attribute was not found'; } switch (getRowMode()) { case 'fixed': return applyDefaultUnit(rowHeight); case 'ratio': var whRatio = rowHeight.split(':'); return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); case 'fit': return 0; // N/A } } function getRowMode() { var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); if (!rowHeight) { throw 'md-grid-list: md-row-height attribute was not found'; } if (rowHeight == 'fit') { return 'fit'; } else if (rowHeight.indexOf(':') !== -1) { return 'ratio'; } else { return 'fixed'; } } function applyDefaultUnit(val) { return /\D$/.test(val) ? val : val + 'px'; } } } /* @ngInject */ function GridListController($mdUtil) { this.layoutInvalidated = false; this.tilesInvalidated = false; this.$timeout_ = $mdUtil.nextTick; this.layoutDelegate = angular.noop; } GridListController.prototype = { invalidateTiles: function() { this.tilesInvalidated = true; this.invalidateLayout(); }, invalidateLayout: function() { if (this.layoutInvalidated) { return; } this.layoutInvalidated = true; this.$timeout_(angular.bind(this, this.layout)); }, layout: function() { try { this.layoutDelegate(this.tilesInvalidated); } finally { this.layoutInvalidated = false; this.tilesInvalidated = false; } } }; /* @ngInject */ function GridLayoutFactory($mdUtil) { var defaultAnimator = GridTileAnimator; /** * Set the reflow animator callback */ GridLayout.animateWith = function(customAnimator) { defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; }; return GridLayout; /** * Publish layout function */ function GridLayout(colCount, tileSpans) { var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; layoutTime = $mdUtil.time(function() { layoutInfo = calculateGridFor(colCount, tileSpans); }); return self = { /** * An array of objects describing each tile's position in the grid. */ layoutInfo: function() { return layoutInfo; }, /** * Maps grid positioning to an element and a set of styles using the * provided updateFn. */ map: function(updateFn) { mapTime = $mdUtil.time(function() { var info = self.layoutInfo(); gridStyles = updateFn(info.positioning, info.rowCount); }); return self; }, /** * Default animator simply sets the element.css( ). An alternate * animator can be provided as an argument. The function has the following * signature: * * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) */ reflow: function(animatorFn) { reflowTime = $mdUtil.time(function() { var animator = animatorFn || defaultAnimator; animator(gridStyles.grid, gridStyles.tiles); }); return self; }, /** * Timing for the most recent layout run. */ performance: function() { return { tileCount: tileSpans.length, layoutTime: layoutTime, mapTime: mapTime, reflowTime: reflowTime, totalTime: layoutTime + mapTime + reflowTime }; } }; } /** * Default Gridlist animator simple sets the css for each element; * NOTE: any transitions effects must be manually set in the CSS. * e.g. * * md-grid-tile { * transition: all 700ms ease-out 50ms; * } * */ function GridTileAnimator(grid, tiles) { grid.element.css(grid.style); tiles.forEach(function(t) { t.element.css(t.style); }) } /** * Calculates the positions of tiles. * * The algorithm works as follows: * An Array with length colCount (spaceTracker) keeps track of * available tiling positions, where elements of value 0 represents an * empty position. Space for a tile is reserved by finding a sequence of * 0s with length <= than the tile's colspan. When such a space has been * found, the occupied tile positions are incremented by the tile's * rowspan value, as these positions have become unavailable for that * many rows. * * If the end of a row has been reached without finding space for the * tile, spaceTracker's elements are each decremented by 1 to a minimum * of 0. Rows are searched in this fashion until space is found. */ function calculateGridFor(colCount, tileSpans) { var curCol = 0, curRow = 0, spaceTracker = newSpaceTracker(); return { positioning: tileSpans.map(function(spans, i) { return { spans: spans, position: reserveSpace(spans, i) }; }), rowCount: curRow + Math.max.apply(Math, spaceTracker) }; function reserveSpace(spans, i) { if (spans.col > colCount) { throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + '(' + spans.col + ') that exceeds the column count ' + '(' + colCount + ')'; } var start = 0, end = 0; // TODO(shyndman): This loop isn't strictly necessary if you can // determine the minimum number of rows before a space opens up. To do // this, recognize that you've iterated across an entire row looking for // space, and if so fast-forward by the minimum rowSpan count. Repeat // until the required space opens up. while (end - start < spans.col) { if (curCol >= colCount) { nextRow(); continue; } start = spaceTracker.indexOf(0, curCol); if (start === -1 || (end = findEnd(start + 1)) === -1) { start = end = 0; nextRow(); continue; } curCol = end + 1; } adjustRow(start, spans.col, spans.row); curCol = start + spans.col; return { col: start, row: curRow }; } function nextRow() { curCol = 0; curRow++; adjustRow(0, colCount, -1); // Decrement row spans by one } function adjustRow(from, cols, by) { for (var i = from; i < from + cols; i++) { spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); } } function findEnd(start) { var i; for (i = start; i < spaceTracker.length; i++) { if (spaceTracker[i] !== 0) { return i; } } if (i === spaceTracker.length) { return i; } } function newSpaceTracker() { var tracker = []; for (var i = 0; i < colCount; i++) { tracker.push(0); } return tracker; } } } /** * @ngdoc directive * @name mdGridTile * @module material.components.gridList * @restrict E * @description * Tiles contain the content of an `md-grid-list`. They span one or more grid * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to * display secondary content. * * ### Responsive Attributes * * The `md-grid-tile` directive supports "responsive" attributes, which allow * different `md-rowspan` and `md-colspan` values depending on the currently * matching media query. * * In order to set a responsive attribute, first define the fallback value with * the standard attribute name, then add additional attributes with the * following convention: `{base-attribute-name}-{media-query-name}="{value}"` * (ie. `md-colspan-sm="4"`) * * @param {number=} md-colspan The number of columns to span (default 1). Cannot * exceed the number of columns in the grid. Supports interpolation. * @param {number=} md-rowspan The number of rows to span (default 1). Supports * interpolation. * * @usage * With header: * * * *

This is a header

*
*
*
* * With footer: * * * *

This is a footer

*
*
*
* * Spanning multiple rows/columns: * * * * * * Responsive attributes: * * * * */ function GridTileDirective($mdMedia) { return { restrict: 'E', require: '^mdGridList', template: '
', transclude: true, scope: {}, // Simple controller that exposes attributes to the grid directive controller: ["$attrs", function($attrs) { this.$attrs = $attrs; }], link: postLink }; function postLink(scope, element, attrs, gridCtrl) { // Apply semantics element.attr('role', 'listitem'); // If our colspan or rowspan changes, trigger a layout var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); // Tile registration/deregistration gridCtrl.invalidateTiles(); scope.$on('$destroy', function() { // Mark the tile as destroyed so it is no longer considered in layout, // even if the DOM element sticks around (like during a leave animation) element[0].$$mdDestroyed = true; unwatchAttrs(); gridCtrl.invalidateLayout(); }); if (angular.isDefined(scope.$parent.$index)) { scope.$watch(function() { return scope.$parent.$index; }, function indexChanged(newIdx, oldIdx) { if (newIdx === oldIdx) { return; } gridCtrl.invalidateTiles(); }); } } } function GridTileCaptionDirective() { return { template: '
', transclude: true }; } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.input */ mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"]; mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"]; placeholderDirective.$inject = ["$compile"]; ngMessageDirective.$inject = ["$mdUtil"]; mdSelectOnFocusDirective.$inject = ["$timeout"]; mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; var inputModule = angular.module('material.components.input', [ 'material.core' ]) .directive('mdInputContainer', mdInputContainerDirective) .directive('label', labelDirective) .directive('input', inputTextareaDirective) .directive('textarea', inputTextareaDirective) .directive('mdMaxlength', mdMaxlengthDirective) .directive('placeholder', placeholderDirective) .directive('ngMessages', ngMessagesDirective) .directive('ngMessage', ngMessageDirective) .directive('ngMessageExp', ngMessageDirective) .directive('mdSelectOnFocus', mdSelectOnFocusDirective) .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) .animation('.md-input-messages-animation', ngMessagesAnimation) .animation('.md-input-message-animation', ngMessageAnimation); // If we are running inside of tests; expose some extra services so that we can test them if (window._mdMocksIncluded) { inputModule.service('$$mdInput', function() { return { // special accessor to internals... useful for testing messages: { show : showInputMessages, hide : hideInputMessages, getElement : getMessagesElement } } }) // Register a service for each animation so that we can easily inject them into unit tests .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation) .service('mdInputMessagesAnimation', ngMessagesAnimation) .service('mdInputMessageAnimation', ngMessageAnimation); } /** * @ngdoc directive * @name mdInputContainer * @module material.components.input * * @restrict E * * @description * `` is the parent of any input or textarea element. * * Input and textarea elements will not behave properly unless the md-input-container * parent is provided. * * A single `` should contain only one `` element, otherwise it will throw an error. * * Exception: Hidden inputs (``) are ignored and will not throw an error, so * you may combine these with other inputs. * * Note: When using `ngMessages` with your input element, make sure the message and container elements * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element. * * @param md-is-error {expression=} When the given expression evaluates to true, the input container * will go into error state. Defaults to erroring if the input has been touched and is invalid. * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating * labels. * * @usage * * * * * * * * * * * * * * *

When disabling floating labels

* * * * * * * */ function mdInputContainerDirective($mdTheming, $parse) { ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); }, []).join(","); var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); }, []).join(","); return { restrict: 'E', compile: compile, controller: ContainerCtrl }; function compile(tElement) { // Check for both a left & right icon var leftIcon = tElement[0].querySelector(LEFT_SELECTORS); var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS); if (leftIcon) { tElement.addClass('md-icon-left'); } if (rightIcon) { tElement.addClass('md-icon-right'); } return function postLink(scope, element) { $mdTheming(element); }; } function ContainerCtrl($scope, $element, $attrs, $animate) { var self = this; self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); self.delegateClick = function() { self.input.focus(); }; self.element = $element; self.setFocused = function(isFocused) { $element.toggleClass('md-input-focused', !!isFocused); }; self.setHasValue = function(hasValue) { $element.toggleClass('md-input-has-value', !!hasValue); }; self.setHasPlaceholder = function(hasPlaceholder) { $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); }; self.setInvalid = function(isInvalid) { if (isInvalid) { $animate.addClass($element, 'md-input-invalid'); } else { $animate.removeClass($element, 'md-input-invalid'); } }; $scope.$watch(function() { return self.label && self.input; }, function(hasLabelAndInput) { if (hasLabelAndInput && !self.label.attr('for')) { self.label.attr('for', self.input.attr('id')); } }); } } function labelDirective() { return { restrict: 'E', require: '^?mdInputContainer', link: function(scope, element, attr, containerCtrl) { if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; containerCtrl.label = element; scope.$on('$destroy', function() { containerCtrl.label = null; }); } }; } /** * @ngdoc directive * @name mdInput * @restrict E * @module material.components.input * * @description * You can use any `` or ` *
*
This is required!
*
That's too long!
*
*
* * * * * * * * * *

Notes

* * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). * * The `md-input` and `md-input-container` directives use very specific positioning to achieve the * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the * `` tags. Instead, use relative or absolute positioning. * * *

Textarea directive

* The `textarea` element within a `md-input-container` has the following specific behavior: * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` * attribute. * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text * high initially. If no rows are specified, the directive defaults to 1. * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope. * - If you want a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. */ function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { return { restrict: 'E', require: ['^?mdInputContainer', '?ngModel', '?^form'], link: postLink }; function postLink(scope, element, attr, ctrls) { var containerCtrl = ctrls[0]; var hasNgModel = !!ctrls[1]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); var parentForm = ctrls[2]; var isReadonly = angular.isDefined(attr.readonly); var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); var tagName = element[0].tagName.toLowerCase(); if (!containerCtrl) return; if (attr.type === 'hidden') { element.attr('aria-hidden', 'true'); return; } else if (containerCtrl.input) { if (containerCtrl.input[0].contains(element[0])) { return; } else { throw new Error(" can only have *one* , * * * */ function mdSelectOnFocusDirective($timeout) { return { restrict: 'A', link: postLink }; function postLink(scope, element, attr) { if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; var preventMouseUp = false; element .on('focus', onFocus) .on('mouseup', onMouseUp); scope.$on('$destroy', function() { element .off('focus', onFocus) .off('mouseup', onMouseUp); }); function onFocus() { preventMouseUp = true; $timeout(function() { // Use HTMLInputElement#select to fix firefox select issues. // The debounce is here for Edge's sake, otherwise the selection doesn't work. element[0].select(); // This should be reset from inside the `focus`, because the event might // have originated from something different than a click, e.g. a keyboard event. preventMouseUp = false; }, 1, false); } // Prevents the default action of the first `mouseup` after a focus. // This is necessary, because browsers fire a `mouseup` right after the element // has been focused. In some browsers (Firefox in particular) this can clear the // selection. There are examples of the problem in issue #7487. function onMouseUp(event) { if (preventMouseUp) { event.preventDefault(); } } } } var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; function ngMessagesDirective() { return { restrict: 'EA', link: postLink, // This is optional because we don't want target *all* ngMessage instances, just those inside of // mdInputContainer. require: '^^?mdInputContainer' }; function postLink(scope, element, attrs, inputContainer) { // If we are not a child of an input container, don't do anything if (!inputContainer) return; // Add our animation class element.toggleClass('md-input-messages-animation', true); // Add our md-auto-hide class to automatically hide/show messages when container is invalid element.toggleClass('md-auto-hide', true); // If we see some known visibility directives, remove the md-auto-hide class if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { element.toggleClass('md-auto-hide', false); } } function hasVisibiltyDirective(attrs) { return visibilityDirectives.some(function(attr) { return attrs[attr]; }); } } function ngMessageDirective($mdUtil) { return { restrict: 'EA', compile: compile, priority: 100 }; function compile(tElement) { if (!isInsideInputContainer(tElement)) { // When the current element is inside of a document fragment, then we need to check for an input-container // in the postLink, because the element will be later added to the DOM and is currently just in a temporary // fragment, which causes the input-container check to fail. if (isInsideFragment()) { return function (scope, element) { if (isInsideInputContainer(element)) { // Inside of the postLink function, a ngMessage directive will be a comment element, because it's // currently hidden. To access the shown element, we need to use the element from the compile function. initMessageElement(tElement); } }; } } else { initMessageElement(tElement); } function isInsideFragment() { var nextNode = tElement[0]; while (nextNode = nextNode.parentNode) { if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return true; } } return false; } function isInsideInputContainer(element) { return !!$mdUtil.getClosest(element, "md-input-container"); } function initMessageElement(element) { // Add our animation class element.toggleClass('md-input-message-animation', true); } } } var $$AnimateRunner, $animateCss, $mdUtil, $log; function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); return { addClass: function(element, className, done) { showInputMessages(element, done); } // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire }; } function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); return { enter: function(element, done) { showInputMessages(element, done); }, leave: function(element, done) { hideInputMessages(element, done); }, addClass: function(element, className, done) { if (className == "ng-hide") { hideInputMessages(element, done); } else { done(); } }, removeClass: function(element, className, done) { if (className == "ng-hide") { showInputMessages(element, done); } else { done(); } } }; } function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); return { enter: function(element, done) { var animator = showMessage(element); animator.start().done(done); }, leave: function(element, done) { var animator = hideMessage(element); animator.start().done(done); } }; } function showInputMessages(element, done) { var animators = [], animator; var messages = getMessagesElement(element); var children = messages.children(); if (messages.length == 0 || children.length == 0) { $log.warn('mdInput messages show animation called on invalid messages element: ', element); done(); return; } angular.forEach(children, function(child) { animator = showMessage(angular.element(child)); animators.push(animator.start()); }); $$AnimateRunner.all(animators, done); } function hideInputMessages(element, done) { var animators = [], animator; var messages = getMessagesElement(element); var children = messages.children(); if (messages.length == 0 || children.length == 0) { $log.warn('mdInput messages hide animation called on invalid messages element: ', element); done(); return; } angular.forEach(children, function(child) { animator = hideMessage(angular.element(child)); animators.push(animator.start()); }); $$AnimateRunner.all(animators, done); } function showMessage(element) { var height = parseInt(window.getComputedStyle(element[0]).height); var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop); var messages = getMessagesElement(element); var container = getInputElement(element); // Check to see if the message is already visible so we can skip var alreadyVisible = (topMargin > -height); // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) { return $animateCss(element, {}); } return $animateCss(element, { event: 'enter', structural: true, from: {"opacity": 0, "margin-top": -height + "px"}, to: {"opacity": 1, "margin-top": "0"}, duration: 0.3 }); } function hideMessage(element) { var height = element[0].offsetHeight; var styles = window.getComputedStyle(element[0]); // If we are already hidden, just return an empty animation if (parseInt(styles.opacity) === 0) { return $animateCss(element, {}); } // Otherwise, animate return $animateCss(element, { event: 'leave', structural: true, from: {"opacity": 1, "margin-top": 0}, to: {"opacity": 0, "margin-top": -height + "px"}, duration: 0.3 }); } function getInputElement(element) { var inputContainer = element.controller('mdInputContainer'); return inputContainer.element; } function getMessagesElement(element) { // If we ARE the messages element, just return ourself if (element.hasClass('md-input-messages-animation')) { return element; } // If we are a ng-message element, we need to traverse up the DOM tree if (element.hasClass('md-input-message-animation')) { return angular.element($mdUtil.getClosest(element, function(node) { return node.classList.contains('md-input-messages-animation'); })); } // Otherwise, we can traverse down return angular.element(element[0].querySelector('.md-input-messages-animation')); } function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) { $$AnimateRunner = _$$AnimateRunner_; $animateCss = _$animateCss_; $mdUtil = _$mdUtil_; $log = _$log_; } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.list * @description * List module */ MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; mdListDirective.$inject = ["$mdTheming"]; mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; angular.module('material.components.list', [ 'material.core' ]) .controller('MdListController', MdListController) .directive('mdList', mdListDirective) .directive('mdListItem', mdListItemDirective); /** * @ngdoc directive * @name mdList * @module material.components.list * * @restrict E * * @description * The `` directive is a list container for 1..n `` tags. * * @usage * * * * *
*

{{item.title}}

*

{{item.description}}

*
*
*
*
*/ function mdListDirective($mdTheming) { return { restrict: 'E', compile: function(tEl) { tEl[0].setAttribute('role', 'list'); return $mdTheming; } }; } /** * @ngdoc directive * @name mdListItem * @module material.components.list * * @restrict E * * @description * A `md-list-item` element can be used to represent some information in a row.
* * @usage * ### Single Row Item * * * Single Row Item * * * * ### Multiple Lines * By using the following markup, you will be able to have two lines inside of one `md-list-item`. * * * *
*

First Line

*

Second Line

*
*
*
* * It is also possible to have three lines inside of one list item. * * * *
*

First Line

*

Second Line

*

Third Line

*
*
*
* * ### Secondary Items * Secondary items are elements which will be aligned at the end of the `md-list-item`. * * * * Single Row Item * * Secondary Button * * * * * It also possible to have multiple secondary items inside of one `md-list-item`. * * * * Single Row Item * First Button * Second Button * * * * ### Proxy Item * Proxies are elements, which will execute their specific action on click
* Currently supported proxy items are * - `md-checkbox` (Toggle) * - `md-switch` (Toggle) * - `md-menu` (Open) * * This means, when using a supported proxy item inside of `md-list-item`, the list item will * automatically become clickable and executes the associated action of the proxy element on click. * * It is possible to disable this behavior by applying the `md-no-proxy` class to the list item. * * * * No Proxy List * * * * * Here are a few examples of proxy elements inside of a list item. * * * * First Line * * * * * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click. * * * * First Line * * * * * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`. * * It is also possible to have a `md-menu` inside of a `md-list-item`. * * *

Click anywhere to fire the secondary action

* * * * * * * * Redial * * * * * Check voicemail * * * * * * Notifications * * * * *
*
* * The menu will automatically open, when the users clicks on the `md-list-item`.
* * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the * position mode and applies it to the `md-menu`. * * ### Avatars * Sometimes you may want to have some avatars inside of the `md-list-item `.
* You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `` element. * * * * * Alan Turing * * * When using `` for an avatar, you have to use the `.md-avatar-icon` class. * * * * Timothy Kopra * * * * In cases, you have a `md-list-item`, which doesn't have any avatar, * but you want to align it with the other avatar items, you have to use the `.md-offset` class. * * * * Jon Doe * * * * ### DOM modification * The `md-list-item` component automatically detects if the list item should be clickable. * * --- * If the `md-list-item` is clickable, we wrap all content inside of a `
` and create * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`) * * We create an overlaying button, instead of wrapping all content inside of the button, * because otherwise some elements may not be clickable inside of the button. * * --- * When using a secondary item inside of your list item, the `md-list-item` component will automatically create * a secondary container at the end of the `md-list-item`, which contains all secondary items. * * The secondary item container is not static, because otherwise the overflow will not work properly on the * list item. * */ function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu']; return { restrict: 'E', controller: 'MdListController', compile: function(tEl, tAttrs) { // Check for proxy controls (no ng-click on parent, and a control inside) var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); var hasProxiedElement; var proxyElement; var itemContainer = tEl; tEl[0].setAttribute('role', 'listitem'); if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { wrapIn('button'); } else if (!tEl.hasClass('md-no-proxy')) { for (var i = 0, type; type = proxiedTypes[i]; ++i) { if (proxyElement = tEl[0].querySelector(type)) { hasProxiedElement = true; break; } } if (hasProxiedElement) { wrapIn('div'); } else { tEl.addClass('md-no-proxy'); } } wrapSecondaryItems(); setupToggleAria(); if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") { setupProxiedMenu(); } function setupToggleAria() { var toggleTypes = ['md-switch', 'md-checkbox']; var toggle; for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { if (toggle = tEl.find(toggleType)[0]) { if (!toggle.hasAttribute('aria-label')) { var p = tEl.find('p')[0]; if (!p) return; toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); } } } } function setupProxiedMenu() { var menuEl = angular.element(proxyElement); var isEndAligned = menuEl.parent().hasClass('md-secondary-container') || proxyElement.parentNode.firstElementChild !== proxyElement; var xAxisPosition = 'left'; if (isEndAligned) { // When the proxy item is aligned at the end of the list, we have to set the origin to the end. xAxisPosition = 'right'; } // Set the position mode / origin of the proxied menu. if (!menuEl.attr('md-position-mode')) { menuEl.attr('md-position-mode', xAxisPosition + ' target'); } // Apply menu open binding to menu button var menuOpenButton = menuEl.children().eq(0); if (!hasClickEvent(menuOpenButton[0])) { menuOpenButton.attr('ng-click', '$mdMenu.open($event)'); } if (!menuOpenButton.attr('aria-label')) { menuOpenButton.attr('aria-label', 'Open List Menu'); } } function wrapIn(type) { if (type == 'div') { itemContainer = angular.element('
'); itemContainer.append(tEl.contents()); tEl.addClass('md-proxy-focus'); } else { // Element which holds the default list-item content. itemContainer = angular.element( '
'+ '
'+ '
' ); // Button which shows ripple and executes primary action. var buttonWrap = angular.element( '' ); copyAttributes(tEl[0], buttonWrap[0]); // If there is no aria-label set on the button (previously copied over if present) // we determine the label from the content and copy it to the button. if (!buttonWrap.attr('aria-label')) { buttonWrap.attr('aria-label', $mdAria.getText(tEl)); } // We allow developers to specify the `md-no-focus` class, to disable the focus style // on the button executor. Once more classes should be forwarded, we should probably make the // class forward more generic. if (tEl.hasClass('md-no-focus')) { buttonWrap.addClass('md-no-focus'); } // Append the button wrap before our list-item content, because it will overlay in relative. itemContainer.prepend(buttonWrap); itemContainer.children().eq(1).append(tEl.contents()); tEl.addClass('_md-button-wrap'); } tEl[0].setAttribute('tabindex', '-1'); tEl.append(itemContainer); } function wrapSecondaryItems() { var secondaryItemsWrapper = angular.element('
'); angular.forEach(secondaryItems, function(secondaryItem) { wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); }); itemContainer.append(secondaryItemsWrapper); } function wrapSecondaryItem(secondaryItem, container) { // If the current secondary item is not a button, but contains a ng-click attribute, // the secondary item will be automatically wrapped inside of a button. if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { $mdAria.expect(secondaryItem, 'aria-label'); var buttonWrapper = angular.element(''); // Copy the attributes from the secondary item to the generated button. // We also support some additional attributes from the secondary item, // because some developers may use a ngIf, ngHide, ngShow on their item. copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']); secondaryItem.setAttribute('tabindex', '-1'); buttonWrapper.append(secondaryItem); secondaryItem = buttonWrapper[0]; } if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { // In this case we remove the secondary class, so we can identify it later, when we searching for the // proxy items. angular.element(secondaryItem).removeClass('md-secondary'); } tEl.addClass('md-with-secondary'); container.append(secondaryItem); } /** * Copies attributes from a source element to the destination element * By default the function will copy the most necessary attributes, supported * by the button executor for clickable list items. * @param source Element with the specified attributes * @param destination Element which will retrieve the attributes * @param extraAttrs Additional attributes, which will be copied over. */ function copyAttributes(source, destination, extraAttrs) { var copiedAttrs = $mdUtil.prefixer([ 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', 'href', 'ng-href', 'rel', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' ]); if (extraAttrs) { copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs)); } angular.forEach(copiedAttrs, function(attr) { if (source.hasAttribute(attr)) { destination.setAttribute(attr, source.getAttribute(attr)); source.removeAttribute(attr); } }); } function isProxiedElement(el) { return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; } function isButton(el) { var nodeName = el.nodeName.toUpperCase(); return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; } function hasClickEvent (element) { var attr = element.attributes; for (var i = 0; i < attr.length; i++) { if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; } return false; } return postLink; function postLink($scope, $element, $attr, ctrl) { $element.addClass('_md'); // private md component indicator for styling var proxies = [], firstElement = $element[0].firstElementChild, isButtonWrap = $element.hasClass('_md-button-wrap'), clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, hasClick = clickChild && hasClickEvent(clickChild), noProxies = $element.hasClass('md-no-proxy'); computeProxies(); computeClickable(); if (proxies.length) { angular.forEach(proxies, function(proxy) { proxy = angular.element(proxy); $scope.mouseActive = false; proxy.on('mousedown', function() { $scope.mouseActive = true; $timeout(function(){ $scope.mouseActive = false; }, 100); }) .on('focus', function() { if ($scope.mouseActive === false) { $element.addClass('md-focused'); } proxy.on('blur', function proxyOnBlur() { $element.removeClass('md-focused'); proxy.off('blur', proxyOnBlur); }); }); }); } function computeProxies() { if (firstElement && firstElement.children && !hasClick && !noProxies) { angular.forEach(proxiedTypes, function(type) { // All elements which are not capable for being used a proxy have the .md-secondary class // applied. These items had been sorted out in the secondary wrap function. angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { proxies.push(child); }); }); } } function computeClickable() { if (proxies.length == 1 || hasClick) { $element.addClass('md-clickable'); if (!hasClick) { ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); } } } function isEventFromControl(event) { var forbiddenControls = ['md-slider']; // If there is no path property in the event, then we can assume that the event was not bubbled. if (!event.path) { return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1; } // We iterate the event path up and check for a possible component. // Our maximum index to search, is the list item root. var maxPath = event.path.indexOf($element.children()[0]); for (var i = 0; i < maxPath; i++) { if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) { return true; } } } var clickChildKeypressListener = function(e) { if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) { var keyCode = e.which || e.keyCode; if (keyCode == $mdConstant.KEY_CODE.SPACE) { if (clickChild) { clickChild.click(); e.preventDefault(); e.stopPropagation(); } } } }; if (!hasClick && !proxies.length) { clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); } $element.off('click'); $element.off('keypress'); if (proxies.length == 1 && clickChild) { $element.children().eq(0).on('click', function(e) { // When the event is coming from an control and it should not trigger the proxied element // then we are skipping. if (isEventFromControl(e)) return; var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); if (!parentButton && clickChild.contains(e.target)) { angular.forEach(proxies, function(proxy) { if (e.target !== proxy && !proxy.contains(e.target)) { if (proxy.nodeName === 'MD-MENU') { proxy = proxy.children[0]; } angular.element(proxy).triggerHandler('click'); } }); } }); } $scope.$on('$destroy', function () { clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); }); } } }; } /* * @private * @ngdoc controller * @name MdListController * @module material.components.list * */ function MdListController($scope, $element, $mdListInkRipple) { var ctrl = this; ctrl.attachRipple = attachRipple; function attachRipple (scope, element) { var options = {}; $mdListInkRipple.attach(scope, element, options); } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.menu */ angular.module('material.components.menu', [ 'material.core', 'material.components.backdrop' ]); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.menuBar */ angular.module('material.components.menuBar', [ 'material.core', 'material.components.icon', 'material.components.menu' ]); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.navBar */ MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; MdNavItem.$inject = ["$mdAria", "$$rAF"]; MdNavItemController.$inject = ["$element"]; MdNavBar.$inject = ["$mdAria", "$mdTheming"]; angular.module('material.components.navBar', ['material.core']) .controller('MdNavBarController', MdNavBarController) .directive('mdNavBar', MdNavBar) .controller('MdNavItemController', MdNavItemController) .directive('mdNavItem', MdNavItem); /***************************************************************************** * PUBLIC DOCUMENTATION * *****************************************************************************/ /** * @ngdoc directive * @name mdNavBar * @module material.components.navBar * * @restrict E * * @description * The `` directive renders a list of material tabs that can be used * for top-level page navigation. Unlike ``, it has no concept of a tab * body and no bar pagination. * * Because it deals with page navigation, certain routing concepts are built-in. * Route changes via via ng-href, ui-sref, or ng-click events are supported. * Alternatively, the user could simply watch currentNavItem for changes. * * Accessibility functionality is implemented as a site navigator with a * listbox, according to * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style * * @param {string=} mdSelectedNavItem The name of the current tab; this must * match the name attribute of `` * @param {boolean=} mdNoInkBar If set to true, the ink bar will be hidden. * @param {string=} navBarAriaLabel An aria-label for the nav-bar * * @usage * * * * Page One * * Page Two * Page Three * * Page Four * * * * * (function() { * 'use strict'; * * $rootScope.$on('$routeChangeSuccess', function(event, current) { * $scope.currentLink = getCurrentLinkFromRoute(current); * }); * }); * */ /***************************************************************************** * mdNavItem *****************************************************************************/ /** * @ngdoc directive * @name mdNavItem * @module material.components.navBar * * @restrict E * * @description * `` describes a page navigation link within the `` * component. It renders an md-button as the actual link. * * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required * to be specified. * * @param {Function=} mdNavClick Function which will be called when the * link is clicked to change the page. Renders as an `ng-click`. * @param {string=} mdNavHref url to transition to when this link is clicked. * Renders as an `ng-href`. * @param {string=} mdNavSref Ui-router state to transition to when this link is * clicked. Renders as a `ui-sref`. * @param {!Object=} srefOpts Ui-router options that are passed to the * `$state.go()` function. See the [Ui-router documentation for details] * (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html). * @param {string=} name The name of this link. Used by the nav bar to know * which link is currently selected. * @param {string=} aria-label Adds alternative text for accessibility * * @usage * See `` for usage. */ /***************************************************************************** * IMPLEMENTATION * *****************************************************************************/ function MdNavBar($mdAria, $mdTheming) { return { restrict: 'E', transclude: true, controller: MdNavBarController, controllerAs: 'ctrl', bindToController: true, scope: { 'mdSelectedNavItem': '=?', 'mdNoInkBar': '=?', 'navBarAriaLabel': '@?', }, template: '
' + '' + '' + '
', link: function(scope, element, attrs, ctrl) { $mdTheming(element); if (!ctrl.navBarAriaLabel) { $mdAria.expectAsync(element, 'aria-label', angular.noop); } }, }; } /** * Controller for the nav-bar component. * * Accessibility functionality is implemented as a site navigator with a * listbox, according to * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style * @param {!angular.JQLite} $element * @param {!angular.Scope} $scope * @param {!angular.Timeout} $timeout * @param {!Object} $mdConstant * @constructor * @final * @ngInject */ function MdNavBarController($element, $scope, $timeout, $mdConstant) { // Injected variables /** @private @const {!angular.Timeout} */ this._$timeout = $timeout; /** @private @const {!angular.Scope} */ this._$scope = $scope; /** @private @const {!Object} */ this._$mdConstant = $mdConstant; // Data-bound variables. /** @type {string} */ this.mdSelectedNavItem; /** @type {string} */ this.navBarAriaLabel; // State variables. /** @type {?angular.JQLite} */ this._navBarEl = $element[0]; /** @type {?angular.JQLite} */ this._inkbar; var self = this; // need to wait for transcluded content to be available var deregisterTabWatch = this._$scope.$watch(function() { return self._navBarEl.querySelectorAll('._md-nav-button').length; }, function(newLength) { if (newLength > 0) { self._initTabs(); deregisterTabWatch(); } }); } /** * Initializes the tab components once they exist. * @private */ MdNavBarController.prototype._initTabs = function() { this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar')); var self = this; this._$timeout(function() { self._updateTabs(self.mdSelectedNavItem, undefined); }); this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) { // Wait a digest before update tabs for products doing // anything dynamic in the template. self._$timeout(function() { self._updateTabs(newValue, oldValue); }); }); }; /** * Set the current tab to be selected. * @param {string|undefined} newValue New current tab name. * @param {string|undefined} oldValue Previous tab name. * @private */ MdNavBarController.prototype._updateTabs = function(newValue, oldValue) { var self = this; var tabs = this._getTabs(); // this._getTabs can return null if nav-bar has not yet been initialized if(!tabs) return; var oldIndex = -1; var newIndex = -1; var newTab = this._getTabByName(newValue); var oldTab = this._getTabByName(oldValue); if (oldTab) { oldTab.setSelected(false); oldIndex = tabs.indexOf(oldTab); } if (newTab) { newTab.setSelected(true); newIndex = tabs.indexOf(newTab); } this._$timeout(function() { self._updateInkBarStyles(newTab, newIndex, oldIndex); }); }; /** * Repositions the ink bar to the selected tab. * @private */ MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { this._inkbar.toggleClass('_md-left', newIndex < oldIndex) .toggleClass('_md-right', newIndex > oldIndex); this._inkbar.css({display: newIndex < 0 ? 'none' : ''}); if (tab) { var tabEl = tab.getButtonEl(); var left = tabEl.offsetLeft; this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); } }; /** * Returns an array of the current tabs. * @return {!Array} * @private */ MdNavBarController.prototype._getTabs = function() { var controllers = Array.prototype.slice.call( this._navBarEl.querySelectorAll('.md-nav-item')) .map(function(el) { return angular.element(el).controller('mdNavItem') }); return controllers.indexOf(undefined) ? controllers : null; }; /** * Returns the tab with the specified name. * @param {string} name The name of the tab, found in its name attribute. * @return {!NavItemController|undefined} * @private */ MdNavBarController.prototype._getTabByName = function(name) { return this._findTab(function(tab) { return tab.getName() == name; }); }; /** * Returns the selected tab. * @return {!NavItemController|undefined} * @private */ MdNavBarController.prototype._getSelectedTab = function() { return this._findTab(function(tab) { return tab.isSelected(); }); }; /** * Returns the focused tab. * @return {!NavItemController|undefined} */ MdNavBarController.prototype.getFocusedTab = function() { return this._findTab(function(tab) { return tab.hasFocus(); }); }; /** * Find a tab that matches the specified function. * @private */ MdNavBarController.prototype._findTab = function(fn) { var tabs = this._getTabs(); for (var i = 0; i < tabs.length; i++) { if (fn(tabs[i])) { return tabs[i]; } } return null; }; /** * Direct focus to the selected tab when focus enters the nav bar. */ MdNavBarController.prototype.onFocus = function() { var tab = this._getSelectedTab(); if (tab) { tab.setFocused(true); } }; /** * Move focus from oldTab to newTab. * @param {!NavItemController} oldTab * @param {!NavItemController} newTab * @private */ MdNavBarController.prototype._moveFocus = function(oldTab, newTab) { oldTab.setFocused(false); newTab.setFocused(true); }; /** * Responds to keypress events. * @param {!Event} e */ MdNavBarController.prototype.onKeydown = function(e) { var keyCodes = this._$mdConstant.KEY_CODE; var tabs = this._getTabs(); var focusedTab = this.getFocusedTab(); if (!focusedTab) return; var focusedTabIndex = tabs.indexOf(focusedTab); // use arrow keys to navigate between tabs switch (e.keyCode) { case keyCodes.UP_ARROW: case keyCodes.LEFT_ARROW: if (focusedTabIndex > 0) { this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]); } break; case keyCodes.DOWN_ARROW: case keyCodes.RIGHT_ARROW: if (focusedTabIndex < tabs.length - 1) { this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]); } break; case keyCodes.SPACE: case keyCodes.ENTER: // timeout to avoid a "digest already in progress" console error this._$timeout(function() { focusedTab.getButtonEl().click(); }); break; } }; /** * @ngInject */ function MdNavItem($mdAria, $$rAF) { return { restrict: 'E', require: ['mdNavItem', '^mdNavBar'], controller: MdNavItemController, bindToController: true, controllerAs: 'ctrl', replace: true, transclude: true, template: function(tElement, tAttrs) { var hasNavClick = tAttrs.mdNavClick; var hasNavHref = tAttrs.mdNavHref; var hasNavSref = tAttrs.mdNavSref; var hasSrefOpts = tAttrs.srefOpts; var navigationAttribute; var navigationOptions; var buttonTemplate; // Cannot specify more than one nav attribute if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { throw Error( 'Must not specify more than one of the md-nav-click, md-nav-href, ' + 'or md-nav-sref attributes per nav-item directive.' ); } if (hasNavClick) { navigationAttribute = 'ng-click="ctrl.mdNavClick()"'; } else if (hasNavHref) { navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"'; } else if (hasNavSref) { navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"'; } navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : ''; if (navigationAttribute) { buttonTemplate = '' + '' + '' + ''; } return '' + '
  • ' + (buttonTemplate || '') + '
  • '; }, scope: { 'mdNavClick': '&?', 'mdNavHref': '@?', 'mdNavSref': '@?', 'srefOpts': '=?', 'name': '@', }, link: function(scope, element, attrs, controllers) { // When accessing the element's contents synchronously, they // may not be defined yet because of transclusion. There is a higher // chance that it will be accessible if we wait one frame. $$rAF(function() { var mdNavItem = controllers[0]; var mdNavBar = controllers[1]; var navButton = angular.element(element[0].querySelector('._md-nav-button')); if (!mdNavItem.name) { mdNavItem.name = angular.element(element[0] .querySelector('._md-nav-button-text')).text().trim(); } navButton.on('click', function() { mdNavBar.mdSelectedNavItem = mdNavItem.name; scope.$apply(); }); $mdAria.expectWithText(element, 'aria-label'); }); } }; } /** * Controller for the nav-item component. * @param {!angular.JQLite} $element * @constructor * @final * @ngInject */ function MdNavItemController($element) { /** @private @const {!angular.JQLite} */ this._$element = $element; // Data-bound variables /** @const {?Function} */ this.mdNavClick; /** @const {?string} */ this.mdNavHref; /** @const {?string} */ this.mdNavSref; /** @const {?Object} */ this.srefOpts; /** @const {?string} */ this.name; // State variables /** @private {boolean} */ this._selected = false; /** @private {boolean} */ this._focused = false; } /** * Returns a map of class names and values for use by ng-class. * @return {!Object} */ MdNavItemController.prototype.getNgClassMap = function() { return { 'md-active': this._selected, 'md-primary': this._selected, 'md-unselected': !this._selected, 'md-focused': this._focused, }; }; /** * Get the name attribute of the tab. * @return {string} */ MdNavItemController.prototype.getName = function() { return this.name; }; /** * Get the button element associated with the tab. * @return {!Element} */ MdNavItemController.prototype.getButtonEl = function() { return this._$element[0].querySelector('._md-nav-button'); }; /** * Set the selected state of the tab. * @param {boolean} isSelected */ MdNavItemController.prototype.setSelected = function(isSelected) { this._selected = isSelected; }; /** * @return {boolean} */ MdNavItemController.prototype.isSelected = function() { return this._selected; }; /** * Set the focused state of the tab. * @param {boolean} isFocused */ MdNavItemController.prototype.setFocused = function(isFocused) { this._focused = isFocused; if (isFocused) { this.getButtonEl().focus(); } }; /** * @return {boolean} */ MdNavItemController.prototype.hasFocus = function() { return this._focused; }; })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.panel */ MdPanelService.$inject = ["presets", "$rootElement", "$rootScope", "$injector", "$window"]; angular .module('material.components.panel', [ 'material.core', 'material.components.backdrop' ]) .provider('$mdPanel', MdPanelProvider); /***************************************************************************** * PUBLIC DOCUMENTATION * *****************************************************************************/ /** * @ngdoc service * @name $mdPanelProvider * @module material.components.panel * * @description * `$mdPanelProvider` allows users to create configuration presets that will be * stored within a cached presets object. When the configuration is needed, the * user can request the preset by passing it as the first parameter in the * `$mdPanel.create` or `$mdPanel.open` methods. * * @usage * * (function(angular, undefined) { * 'use strict'; * * angular * .module('demoApp', ['ngMaterial']) * .config(DemoConfig) * .controller('DemoCtrl', DemoCtrl) * .controller('DemoMenuCtrl', DemoMenuCtrl); * * function DemoConfig($mdPanelProvider) { * $mdPanelProvider.definePreset('demoPreset', { * attachTo: angular.element(document.body), * controller: DemoMenuCtrl, * controllerAs: 'ctrl', * template: '' + * '', * panelClass: 'menu-panel-container', * focusOnOpen: false, * zIndex: 100, * propagateContainerEvents: true, * groupName: 'menus' * }); * } * * function PanelProviderCtrl($mdPanel) { * this.navigation = { * name: 'navigation', * items: [ * 'Home', * 'About', * 'Contact' * ] * }; * this.favorites = { * name: 'favorites', * items: [ * 'Add to Favorites' * ] * }; * this.more = { * name: 'more', * items: [ * 'Account', * 'Sign Out' * ] * }; * * $mdPanel.newPanelGroup('menus', { * maxOpen: 2 * }); * * this.showMenu = function($event, menu) { * $mdPanel.open('demoPreset', { * id: 'menu_' + menu.name, * position: $mdPanel.newPanelPosition() * .relativeTo($event.srcElement) * .addPanelPosition( * $mdPanel.xPosition.ALIGN_START, * $mdPanel.yPosition.BELOW * ), * locals: { * items: menu.items * }, * openFrom: $event * }); * }; * } * * function PanelMenuCtrl(mdPanelRef) { * // The controller is provided with an import named 'mdPanelRef' * this.closeMenu = function() { * mdPanelRef && mdPanelRef.close(); * }; * } * })(angular); * */ /** * @ngdoc method * @name $mdPanelProvider#definePreset * @description * Takes the passed in preset name and preset configuration object and adds it * to the `_presets` object of the provider. This `_presets` object is then * passed along to the `$mdPanel` service. * * @param {string} name Preset name. * @param {!Object} preset Specific configuration object that can contain any * and all of the parameters avaialble within the `$mdPanel.create` method. * However, parameters that pertain to id, position, animation, and user * interaction are not allowed and will be removed from the preset * configuration. */ /***************************************************************************** * MdPanel Service * *****************************************************************************/ /** * @ngdoc service * @name $mdPanel * @module material.components.panel * * @description * `$mdPanel` is a robust, low-level service for creating floating panels on * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc. * * @usage * * (function(angular, undefined) { * 'use strict'; * * angular * .module('demoApp', ['ngMaterial']) * .controller('DemoDialogController', DialogController); * * var panelRef; * * function showPanel($event) { * var panelPosition = $mdPanel.newPanelPosition() * .absolute() * .top('50%') * .left('50%'); * * var panelAnimation = $mdPanel.newPanelAnimation() * .targetEvent($event) * .defaultAnimation('md-panel-animate-fly') * .closeTo('.show-button'); * * var config = { * attachTo: angular.element(document.body), * controller: DialogController, * controllerAs: 'ctrl', * position: panelPosition, * animation: panelAnimation, * targetEvent: $event, * templateUrl: 'dialog-template.html', * clickOutsideToClose: true, * escapeToClose: true, * focusOnOpen: true * }; * * $mdPanel.open(config) * .then(function(result) { * panelRef = result; * }); * } * * function DialogController(MdPanelRef) { * function closeDialog() { * if (MdPanelRef) MdPanelRef.close(); * } * } * })(angular); * */ /** * @ngdoc method * @name $mdPanel#create * @description * Creates a panel with the specified options. * * @param config {!Object=} Specific configuration object that may contain the * following properties: * * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided, * the created panel is added to a tracked panels object. Any subsequent * requests made to create a panel with that ID are ignored. This is useful * in having the panel service not open multiple panels from the same user * interaction when there is no backdrop and events are propagated. Defaults * to an arbitrary string that is not tracked. * - `template` - `{string=}`: HTML template to show in the panel. This * **must** be trusted HTML with respect to AngularJS’s * [$sce service](https://docs.angularjs.org/api/ng/service/$sce). * - `templateUrl` - `{string=}`: The URL that will be used as the content of * the panel. * - `contentElement` - `{(string|!angular.JQLite|!Element)=}`: Pre-compiled * element to be used as the panel's content. * - `controller` - `{(function|string)=}`: The controller to associate with * the panel. The controller can inject a reference to the returned * panelRef, which allows the panel to be closed, hidden, and shown. Any * fields passed in through locals or resolve will be bound to the * controller. * - `controllerAs` - `{string=}`: An alias to assign the controller to on * the scope. * - `bindToController` - `{boolean=}`: Binds locals to the controller * instead of passing them in. Defaults to true, as this is a best * practice. * - `locals` - `{Object=}`: An object containing key/value pairs. The keys * will be used as names of values to inject into the controller. For * example, `locals: {three: 3}` would inject `three` into the controller, * with the value 3. 'mdPanelRef' is a reserved key, and will always * be set to the created MdPanelRef instance. * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as * values. The panel will not open until all of the promises resolve. * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to * attach the panel to. Defaults to appending to the root element of the * application. * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch * events should be allowed to propagate 'go through' the container, aka the * wrapper, of the panel. Defaults to false. * - `panelClass` - `{string=}`: A css class to apply to the panel element. * This class should define any borders, box-shadow, etc. for the panel. * - `zIndex` - `{number=}`: The z-index to place the panel at. * Defaults to 80. * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that * specifies the alignment of the panel. For more information, see * `MdPanelPosition`. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click * outside the panel to close it. Defaults to false. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to * close the panel. Defaults to false. * - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is * called after the close successfully finishes. The first parameter passed * into this function is the current panelRef and the 2nd is an optional * string explaining the close reason. The currently supported closeReasons * can be found in the MdPanelRef.closeReasons enum. These are by default * passed along by the panel. * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the * panel. If `trapFocus` is true, the user will not be able to interact * with the rest of the page until the panel is dismissed. Defaults to * false. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on * open. Only disable if focusing some other way, as focus management is * required for panels to be accessible. Defaults to true. * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen. * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults * to false. * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that * specifies the animation of the panel. For more information, see * `MdPanelAnimation`. * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop * behind the panel. Defaults to false. * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the * page behind the panel. Defaults to false. * - `onDomAdded` - `{function=}`: Callback function used to announce when * the panel is added to the DOM. * - `onOpenComplete` - `{function=}`: Callback function used to announce * when the open() action is finished. * - `onRemoving` - `{function=}`: Callback function used to announce the * close/hide() action is starting. * - `onDomRemoved` - `{function=}`: Callback function used to announce when * the panel is removed from the DOM. * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus * on when the panel closes. This is commonly the element which triggered * the opening of the panel. If you do not use `origin`, you need to control * the focus manually. * - `groupName` - `{(string|!Array)=}`: A group name or an array of * group names. The group name is used for creating a group of panels. The * group is used for configuring the number of open panels and identifying * specific behaviors for groups. For instance, all tooltips could be * identified using the same groupName. * * @returns {!MdPanelRef} panelRef */ /** * @ngdoc method * @name $mdPanel#open * @description * Calls the create method above, then opens the panel. This is a shortcut for * creating and then calling open manually. If custom methods need to be * called when the panel is added to the DOM or opened, do not use this method. * Instead create the panel, chain promises on the domAdded and openComplete * methods, and call open from the returned panelRef. * * @param {!Object=} config Specific configuration object that may contain * the properties defined in `$mdPanel.create`. * @returns {!angular.$q.Promise} panelRef A promise that resolves * to an instance of the panel. */ /** * @ngdoc method * @name $mdPanel#newPanelPosition * @description * Returns a new instance of the MdPanelPosition object. Use this to create * the position config object. * * @returns {!MdPanelPosition} panelPosition */ /** * @ngdoc method * @name $mdPanel#newPanelAnimation * @description * Returns a new instance of the MdPanelAnimation object. Use this to create * the animation config object. * * @returns {!MdPanelAnimation} panelAnimation */ /** * @ngdoc method * @name $mdPanel#newPanelGroup * @description * Creates a panel group and adds it to a tracked list of panel groups. * * @param {string} groupName Name of the group to create. * @param {!Object=} config Specific configuration object that may contain the * following properties: * * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to * be open within a defined panel group. * * @returns {!Object, * openPanels: !Array, * maxOpen: number}>} panelGroup */ /** * @ngdoc method * @name $mdPanel#setGroupMaxOpen * @description * Sets the maximum number of panels in a group that can be opened at a given * time. * * @param {string} groupName The name of the group to configure. * @param {number} maxOpen The maximum number of panels that can be * opened. Infinity can be passed in to remove the maxOpen limit. */ /***************************************************************************** * MdPanelRef * *****************************************************************************/ /** * @ngdoc type * @name MdPanelRef * @module material.components.panel * @description * A reference to a created panel. This reference contains a unique id for the * panel, along with the following properties: * * - `id` - `{string}`: The unique id for the panel. This id is used to track * when a panel was interacted with. * - `config` - `{!Object=}`: The entire config object that was used in * create. * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM. * Visibility to the user does not factor into isAttached. * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the * panel. This property is added in order to have access to the `addClass`, * `removeClass`, `toggleClass`, etc methods. * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added * in order to have access to the `addClass`, `removeClass`, `toggleClass`, * etc methods. */ /** * @ngdoc method * @name MdPanelRef#open * @description * Attaches and shows the panel. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * opened. */ /** * @ngdoc method * @name MdPanelRef#close * @description * Hides and detaches the panel. Note that this will **not** destroy the panel. * If you don't intend on using the panel again, call the {@link #destroy * destroy} method afterwards. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * closed. */ /** * @ngdoc method * @name MdPanelRef#attach * @description * Create the panel elements and attach them to the DOM. The panel will be * hidden by default. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * attached. */ /** * @ngdoc method * @name MdPanelRef#detach * @description * Removes the panel from the DOM. This will NOT hide the panel before removing * it. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * detached. */ /** * @ngdoc method * @name MdPanelRef#show * @description * Shows the panel. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel has * shown and animations are completed. */ /** * @ngdoc method * @name MdPanelRef#hide * @description * Hides the panel. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel has * hidden and animations are completed. */ /** * @ngdoc method * @name MdPanelRef#destroy * @description * Destroys the panel. The panel cannot be opened again after this is called. */ /** * @ngdoc method * @name MdPanelRef#addClass * @deprecated * This method is in the process of being deprecated in favor of using the panel * and container JQLite elements that are referenced in the MdPanelRef object. * Full deprecation is scheduled for material 1.2. * @description * Adds a class to the panel. DO NOT use this hide/show the panel. * * @param {string} newClass class to be added. * @param {boolean} toElement Whether or not to add the class to the panel * element instead of the container. */ /** * @ngdoc method * @name MdPanelRef#removeClass * @deprecated * This method is in the process of being deprecated in favor of using the panel * and container JQLite elements that are referenced in the MdPanelRef object. * Full deprecation is scheduled for material 1.2. * @description * Removes a class from the panel. DO NOT use this to hide/show the panel. * * @param {string} oldClass Class to be removed. * @param {boolean} fromElement Whether or not to remove the class from the * panel element instead of the container. */ /** * @ngdoc method * @name MdPanelRef#toggleClass * @deprecated * This method is in the process of being deprecated in favor of using the panel * and container JQLite elements that are referenced in the MdPanelRef object. * Full deprecation is scheduled for material 1.2. * @description * Toggles a class on the panel. DO NOT use this to hide/show the panel. * * @param {string} toggleClass Class to be toggled. * @param {boolean} onElement Whether or not to remove the class from the panel * element instead of the container. */ /** * @ngdoc method * @name MdPanelRef#updatePosition * @description * Updates the position configuration of a panel. Use this to update the * position of a panel that is open, without having to close and re-open the * panel. * * @param {!MdPanelPosition} position */ /** * @ngdoc method * @name MdPanelRef#addToGroup * @description * Adds a panel to a group if the panel does not exist within the group already. * A panel can only exist within a single group. * * @param {string} groupName The name of the group to add the panel to. */ /** * @ngdoc method * @name MdPanelRef#removeFromGroup * @description * Removes a panel from a group if the panel exists within that group. The group * must be created ahead of time. * * @param {string} groupName The name of the group. */ /** * @ngdoc method * @name MdPanelRef#registerInterceptor * @description * Registers an interceptor with the panel. The callback should return a promise, * which will allow the action to continue when it gets resolved, or will * prevent an action if it is rejected. The interceptors are called sequentially * and it reverse order. `type` must be one of the following * values available on `$mdPanel.interceptorTypes`: * * `CLOSE` - Gets called before the panel begins closing. * * @param {string} type Type of interceptor. * @param {!angular.$q.Promise} callback Callback to be registered. * @returns {!MdPanelRef} */ /** * @ngdoc method * @name MdPanelRef#removeInterceptor * @description * Removes a registered interceptor. * * @param {string} type Type of interceptor to be removed. * @param {function(): !angular.$q.Promise} callback Interceptor to be removed. * @returns {!MdPanelRef} */ /** * @ngdoc method * @name MdPanelRef#removeAllInterceptors * @description * Removes all interceptors. If a type is supplied, only the * interceptors of that type will be cleared. * * @param {string=} type Type of interceptors to be removed. * @returns {!MdPanelRef} */ /** * @ngdoc method * @name MdPanelRef#updateAnimation * @description * Updates the animation configuration for a panel. You can use this to change * the panel's animation without having to re-create it. * * @param {!MdPanelAnimation} animation */ /***************************************************************************** * MdPanelPosition * *****************************************************************************/ /** * @ngdoc type * @name MdPanelPosition * @module material.components.panel * @description * * Object for configuring the position of the panel. * * @usage * * #### Centering the panel * * * new MdPanelPosition().absolute().center(); * * * #### Overlapping the panel with an element * * * new MdPanelPosition() * .relativeTo(someElement) * .addPanelPosition( * $mdPanel.xPosition.ALIGN_START, * $mdPanel.yPosition.ALIGN_TOPS * ); * * * #### Aligning the panel with the bottom of an element * * * new MdPanelPosition() * .relativeTo(someElement) * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW); * */ /** * @ngdoc method * @name MdPanelPosition#absolute * @description * Positions the panel absolutely relative to the parent element. If the parent * is document.body, this is equivalent to positioning the panel absolutely * within the viewport. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#relativeTo * @description * Positions the panel relative to a specific element. * * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, * or angular element to position the panel with respect to. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#top * @description * Sets the value of `top` for the panel. Clears any previously set vertical * position. * * @param {string=} top Value of `top`. Defaults to '0'. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#bottom * @description * Sets the value of `bottom` for the panel. Clears any previously set vertical * position. * * @param {string=} bottom Value of `bottom`. Defaults to '0'. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#start * @description * Sets the panel to the start of the page - `left` if `ltr` or `right` for * `rtl`. Clears any previously set horizontal position. * * @param {string=} start Value of position. Defaults to '0'. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#end * @description * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. * Clears any previously set horizontal position. * * @param {string=} end Value of position. Defaults to '0'. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#left * @description * Sets the value of `left` for the panel. Clears any previously set * horizontal position. * * @param {string=} left Value of `left`. Defaults to '0'. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#right * @description * Sets the value of `right` for the panel. Clears any previously set * horizontal position. * * @param {string=} right Value of `right`. Defaults to '0'. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#centerHorizontally * @description * Centers the panel horizontally in the viewport. Clears any previously set * horizontal position. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#centerVertically * @description * Centers the panel vertically in the viewport. Clears any previously set * vertical position. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#center * @description * Centers the panel horizontally and vertically in the viewport. This is * equivalent to calling both `centerHorizontally` and `centerVertically`. * Clears any previously set horizontal and vertical positions. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#addPanelPosition * @description * Sets the x and y position for the panel relative to another element. Can be * called multiple times to specify an ordered list of panel positions. The * first position which allows the panel to be completely on-screen will be * chosen; the last position will be chose whether it is on-screen or not. * * xPosition must be one of the following values available on * $mdPanel.xPosition: * * * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END * *
     *    *************
     *    *           *
     *    *   PANEL   *
     *    *           *
     *    *************
     *   A B    C    D E
     *
     * A: OFFSET_START (for LTR displays)
     * B: ALIGN_START (for LTR displays)
     * C: CENTER
     * D: ALIGN_END (for LTR displays)
     * E: OFFSET_END (for LTR displays)
     * 
    * * yPosition must be one of the following values available on * $mdPanel.yPosition: * * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW * *
     *   F
     *   G *************
     *     *           *
     *   H *   PANEL   *
     *     *           *
     *   I *************
     *   J
     *
     * F: BELOW
     * G: ALIGN_TOPS
     * H: CENTER
     * I: ALIGN_BOTTOMS
     * J: ABOVE
     * 
    * * @param {string} xPosition * @param {string} yPosition * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#withOffsetX * @description * Sets the value of the offset in the x-direction. * * @param {string} offsetX * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#withOffsetY * @description * Sets the value of the offset in the y-direction. * * @param {string} offsetY * @returns {!MdPanelPosition} */ /***************************************************************************** * MdPanelAnimation * *****************************************************************************/ /** * @ngdoc type * @name MdPanelAnimation * @module material.components.panel * @description * Animation configuration object. To use, create an MdPanelAnimation with the * desired properties, then pass the object as part of $mdPanel creation. * * @usage * * * var panelAnimation = new MdPanelAnimation() * .openFrom(myButtonEl) * .duration(1337) * .closeTo('.my-button') * .withAnimation($mdPanel.animation.SCALE); * * $mdPanel.create({ * animation: panelAnimation * }); * */ /** * @ngdoc method * @name MdPanelAnimation#openFrom * @description * Specifies where to start the open animation. `openFrom` accepts a * click event object, query selector, DOM element, or a Rect object that * is used to determine the bounds. When passed a click event, the location * of the click will be used as the position to start the animation. * * @param {string|!Element|!Event|{top: number, left: number}} * @returns {!MdPanelAnimation} */ /** * @ngdoc method * @name MdPanelAnimation#closeTo * @description * Specifies where to animate the panel close. `closeTo` accepts a * query selector, DOM element, or a Rect object that is used to determine * the bounds. * * @param {string|!Element|{top: number, left: number}} * @returns {!MdPanelAnimation} */ /** * @ngdoc method * @name MdPanelAnimation#withAnimation * @description * Specifies the animation class. * * There are several default animations that can be used: * ($mdPanel.animation) * SLIDE: The panel slides in and out from the specified * elements. It will not fade in or out. * SCALE: The panel scales in and out. Slide and fade are * included in this animation. * FADE: The panel fades in and out. * * Custom classes will by default fade in and out unless * "transition: opacity 1ms" is added to the to custom class. * * @param {string|{open: string, close: string}} cssClass * @returns {!MdPanelAnimation} */ /** * @ngdoc method * @name MdPanelAnimation#duration * @description * Specifies the duration of the animation in milliseconds. The `duration` * method accepts either a number or an object with separate open and close * durations. * * @param {number|{open: number, close: number}} duration * @returns {!MdPanelAnimation} */ /***************************************************************************** * PUBLIC DOCUMENTATION * *****************************************************************************/ var MD_PANEL_Z_INDEX = 80; var MD_PANEL_HIDDEN = '_md-panel-hidden'; var FOCUS_TRAP_TEMPLATE = angular.element( '
    '); var _presets = {}; /** * A provider that is used for creating presets for the panel API. * @final @constructor @ngInject */ function MdPanelProvider() { return { 'definePreset': definePreset, 'getAllPresets': getAllPresets, 'clearPresets': clearPresets, '$get': $getProvider() }; } /** * Takes the passed in panel configuration object and adds it to the `_presets` * object at the specified name. * @param {string} name Name of the preset to set. * @param {!Object} preset Specific configuration object that can contain any * and all of the parameters avaialble within the `$mdPanel.create` method. * However, parameters that pertain to id, position, animation, and user * interaction are not allowed and will be removed from the preset * configuration. */ function definePreset(name, preset) { if (!name || !preset) { throw new Error('mdPanelProvider: The panel preset definition is ' + 'malformed. The name and preset object are required.'); } else if (_presets.hasOwnProperty(name)) { throw new Error('mdPanelProvider: The panel preset you have requested ' + 'has already been defined.'); } // Delete any property on the preset that is not allowed. delete preset.id; delete preset.position; delete preset.animation; _presets[name] = preset; } /** * Gets a clone of the `_presets`. * @return {!Object} */ function getAllPresets() { return angular.copy(_presets); } /** * Clears all of the stored presets. */ function clearPresets() { _presets = {}; } /** * Represents the `$get` method of the AngularJS provider. From here, a new * reference to the MdPanelService is returned where the needed arguments are * passed in including the MdPanelProvider `_presets`. * @param {!Object} _presets * @param {!angular.JQLite} $rootElement * @param {!angular.Scope} $rootScope * @param {!angular.$injector} $injector * @param {!angular.$window} $window */ function $getProvider() { return [ '$rootElement', '$rootScope', '$injector', '$window', function($rootElement, $rootScope, $injector, $window) { return new MdPanelService(_presets, $rootElement, $rootScope, $injector, $window); } ]; } /***************************************************************************** * MdPanel Service * *****************************************************************************/ /** * A service that is used for controlling/displaying panels on the screen. * @param {!Object} presets * @param {!angular.JQLite} $rootElement * @param {!angular.Scope} $rootScope * @param {!angular.$injector} $injector * @param {!angular.$window} $window * @final @constructor @ngInject */ function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) { /** * Default config options for the panel. * Anything angular related needs to be done later. Therefore * scope: $rootScope.$new(true), * attachTo: $rootElement, * are added later. * @private {!Object} */ this._defaultConfigOptions = { bindToController: true, clickOutsideToClose: false, disableParentScroll: false, escapeToClose: false, focusOnOpen: true, fullscreen: false, hasBackdrop: false, propagateContainerEvents: false, transformTemplate: angular.bind(this, this._wrapTemplate), trapFocus: false, zIndex: MD_PANEL_Z_INDEX }; /** @private {!Object} */ this._config = {}; /** @private {!Object} */ this._presets = presets; /** @private @const */ this._$rootElement = $rootElement; /** @private @const */ this._$rootScope = $rootScope; /** @private @const */ this._$injector = $injector; /** @private @const */ this._$window = $window; /** @private @const */ this._$mdUtil = this._$injector.get('$mdUtil'); /** @private {!Object} */ this._trackedPanels = {}; /** * @private {!Object, * openPanels: !Array, * maxOpen: number}>} */ this._groups = Object.create(null); /** * Default animations that can be used within the panel. * @type {enum} */ this.animation = MdPanelAnimation.animation; /** * Possible values of xPosition for positioning the panel relative to * another element. * @type {enum} */ this.xPosition = MdPanelPosition.xPosition; /** * Possible values of yPosition for positioning the panel relative to * another element. * @type {enum} */ this.yPosition = MdPanelPosition.yPosition; /** * Possible values for the interceptors that can be registered on a panel. * @type {enum} */ this.interceptorTypes = MdPanelRef.interceptorTypes; /** * Possible values for closing of a panel. * @type {enum} */ this.closeReasons = MdPanelRef.closeReasons; /** * Possible values of absolute position. * @type {enum} */ this.absPosition = MdPanelPosition.absPosition; } /** * Creates a panel with the specified options. * @param {string=} preset Name of a preset configuration that can be used to * extend the panel configuration. * @param {!Object=} config Configuration object for the panel. * @returns {!MdPanelRef} */ MdPanelService.prototype.create = function(preset, config) { if (typeof preset === 'string') { preset = this._getPresetByName(preset); } else if (typeof preset === 'object' && (angular.isUndefined(config) || !config)) { config = preset; preset = {}; } preset = preset || {}; config = config || {}; // If the passed-in config contains an ID and the ID is within _trackedPanels, // return the tracked panel after updating its config with the passed-in // config. if (angular.isDefined(config.id) && this._trackedPanels[config.id]) { var trackedPanel = this._trackedPanels[config.id]; angular.extend(trackedPanel.config, config); return trackedPanel; } // Combine the passed-in config, the _defaultConfigOptions, and the preset // configuration into the `_config`. this._config = angular.extend({ // If no ID is set within the passed-in config, then create an arbitrary ID. id: config.id || 'panel_' + this._$mdUtil.nextUid(), scope: this._$rootScope.$new(true), attachTo: this._$rootElement }, this._defaultConfigOptions, config, preset); // Create the panelRef and add it to the `_trackedPanels` object. var panelRef = new MdPanelRef(this._config, this._$injector); this._trackedPanels[config.id] = panelRef; // Add the panel to each of its requested groups. if (this._config.groupName) { if (angular.isString(this._config.groupName)) { this._config.groupName = [this._config.groupName]; } angular.forEach(this._config.groupName, function(group) { panelRef.addToGroup(group); }); } this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach)); return panelRef; }; /** * Creates and opens a panel with the specified options. * @param {string=} preset Name of a preset configuration that can be used to * extend the panel configuration. * @param {!Object=} config Configuration object for the panel. * @returns {!angular.$q.Promise} The panel created from create. */ MdPanelService.prototype.open = function(preset, config) { var panelRef = this.create(preset, config); return panelRef.open().then(function() { return panelRef; }); }; /** * Gets a specific preset configuration object saved within `_presets`. * @param {string} preset Name of the preset to search for. * @returns {!Object} The preset configuration object. */ MdPanelService.prototype._getPresetByName = function(preset) { if (!this._presets[preset]) { throw new Error('mdPanel: The panel preset configuration that you ' + 'requested does not exist. Use the $mdPanelProvider to create a ' + 'preset before requesting one.'); } return this._presets[preset]; }; /** * Returns a new instance of the MdPanelPosition. Use this to create the * positioning object. * @returns {!MdPanelPosition} */ MdPanelService.prototype.newPanelPosition = function() { return new MdPanelPosition(this._$injector); }; /** * Returns a new instance of the MdPanelAnimation. Use this to create the * animation object. * @returns {!MdPanelAnimation} */ MdPanelService.prototype.newPanelAnimation = function() { return new MdPanelAnimation(this._$injector); }; /** * Creates a panel group and adds it to a tracked list of panel groups. * @param groupName {string} Name of the group to create. * @param config {!Object=} Specific configuration object that may contain the * following properties: * * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed * open within a defined panel group. * * @returns {!Object, * openPanels: !Array, * maxOpen: number}>} panelGroup */ MdPanelService.prototype.newPanelGroup = function(groupName, config) { if (!this._groups[groupName]) { config = config || {}; var group = { panels: [], openPanels: [], maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity }; this._groups[groupName] = group; } return this._groups[groupName]; }; /** * Sets the maximum number of panels in a group that can be opened at a given * time. * @param {string} groupName The name of the group to configure. * @param {number} maxOpen The maximum number of panels that can be * opened. Infinity can be passed in to remove the maxOpen limit. */ MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) { if (this._groups[groupName]) { this._groups[groupName].maxOpen = maxOpen; } else { throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().'); } }; /** * Determines if the current number of open panels within a group exceeds the * limit of allowed open panels. * @param {string} groupName The name of the group to check. * @returns {boolean} true if open count does exceed maxOpen and false if not. * @private */ MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) { if (this._groups[groupName]) { var group = this._groups[groupName]; return group.maxOpen > 0 && group.openPanels.length > group.maxOpen; } return false; }; /** * Closes the first open panel within a specific group. * @param {string} groupName The name of the group. * @private */ MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) { this._groups[groupName].openPanels[0].close(); }; /** * Wraps the users template in two elements, md-panel-outer-wrapper, which * covers the entire attachTo element, and md-panel, which contains only the * template. This allows the panel control over positioning, animations, * and similar properties. * @param {string} origTemplate The original template. * @returns {string} The wrapped template. * @private */ MdPanelService.prototype._wrapTemplate = function(origTemplate) { var template = origTemplate || ''; // The panel should be initially rendered offscreen so we can calculate // height and width for positioning. return '' + '
    ' + '
    ' + template + '
    ' + '
    '; }; /** * Wraps a content element in a md-panel-outer wrapper and * positions it off-screen. Allows for proper control over positoning * and animations. * @param {!angular.JQLite} contentElement Element to be wrapped. * @return {!angular.JQLite} Wrapper element. * @private */ MdPanelService.prototype._wrapContentElement = function(contentElement) { var wrapper = angular.element('
    '); contentElement.addClass('md-panel _md-panel-offscreen'); wrapper.append(contentElement); return wrapper; }; /***************************************************************************** * MdPanelRef * *****************************************************************************/ /** * A reference to a created panel. This reference contains a unique id for the * panel, along with properties/functions used to control the panel. * @param {!Object} config * @param {!angular.$injector} $injector * @final @constructor */ function MdPanelRef(config, $injector) { // Injected variables. /** @private @const {!angular.$q} */ this._$q = $injector.get('$q'); /** @private @const {!angular.$mdCompiler} */ this._$mdCompiler = $injector.get('$mdCompiler'); /** @private @const {!angular.$mdConstant} */ this._$mdConstant = $injector.get('$mdConstant'); /** @private @const {!angular.$mdUtil} */ this._$mdUtil = $injector.get('$mdUtil'); /** @private @const {!angular.$mdTheming} */ this._$mdTheming = $injector.get('$mdTheming'); /** @private @const {!angular.Scope} */ this._$rootScope = $injector.get('$rootScope'); /** @private @const {!angular.$animate} */ this._$animate = $injector.get('$animate'); /** @private @const {!MdPanelRef} */ this._$mdPanel = $injector.get('$mdPanel'); /** @private @const {!angular.$log} */ this._$log = $injector.get('$log'); /** @private @const {!angular.$window} */ this._$window = $injector.get('$window'); /** @private @const {!Function} */ this._$$rAF = $injector.get('$$rAF'); // Public variables. /** * Unique id for the panelRef. * @type {string} */ this.id = config.id; /** @type {!Object} */ this.config = config; /** @type {!angular.JQLite|undefined} */ this.panelContainer; /** @type {!angular.JQLite|undefined} */ this.panelEl; /** * Whether the panel is attached. This is synchronous. When attach is called, * isAttached is set to true. When detach is called, isAttached is set to * false. * @type {boolean} */ this.isAttached = false; // Private variables. /** @private {Array} */ this._removeListeners = []; /** @private {!angular.JQLite|undefined} */ this._topFocusTrap; /** @private {!angular.JQLite|undefined} */ this._bottomFocusTrap; /** @private {!$mdPanel|undefined} */ this._backdropRef; /** @private {Function?} */ this._restoreScroll = null; /** * Keeps track of all the panel interceptors. * @private {!Object} */ this._interceptors = Object.create(null); /** * Cleanup function, provided by `$mdCompiler` and assigned after the element * has been compiled. When `contentElement` is used, the function is used to * restore the element to it's proper place in the DOM. * @private {!Function} */ this._compilerCleanup = null; /** * Cache for saving and restoring element inline styles, CSS classes etc. * @type {{styles: string, classes: string}} */ this._restoreCache = { styles: '', classes: '' }; } MdPanelRef.interceptorTypes = { CLOSE: 'onClose' }; /** * Opens an already created and configured panel. If the panel is already * visible, does nothing. * @returns {!angular.$q.Promise} A promise that is resolved when * the panel is opened and animations finish. */ MdPanelRef.prototype.open = function() { var self = this; return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var show = self._simpleBind(self.show, self); var checkGroupMaxOpen = function() { if (self.config.groupName) { angular.forEach(self.config.groupName, function(group) { if (self._$mdPanel._openCountExceedsMaxOpen(group)) { self._$mdPanel._closeFirstOpenedPanel(group); } }); } }; self.attach() .then(show) .then(checkGroupMaxOpen) .then(done) .catch(reject); }); }; /** * Closes the panel. * @param {string} closeReason The event type that triggered the close. * @returns {!angular.$q.Promise} A promise that is resolved when * the panel is closed and animations finish. */ MdPanelRef.prototype.close = function(closeReason) { var self = this; return this._$q(function(resolve, reject) { self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() { var done = self._done(resolve, self); var detach = self._simpleBind(self.detach, self); var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop; onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason); self.hide() .then(detach) .then(done) .then(onCloseSuccess) .catch(reject); }, reject); }); }; /** * Attaches the panel. The panel will be hidden afterwards. * @returns {!angular.$q.Promise} A promise that is resolved when * the panel is attached. */ MdPanelRef.prototype.attach = function() { if (this.isAttached && this.panelEl) { return this._$q.when(this); } var self = this; return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var onDomAdded = self.config['onDomAdded'] || angular.noop; var addListeners = function(response) { self.isAttached = true; self._addEventListeners(); return response; }; self._$q.all([ self._createBackdrop(), self._createPanel() .then(addListeners) .catch(reject) ]).then(onDomAdded) .then(done) .catch(reject); }); }; /** * Only detaches the panel. Will NOT hide the panel first. * @returns {!angular.$q.Promise} A promise that is resolved when * the panel is detached. */ MdPanelRef.prototype.detach = function() { if (!this.isAttached) { return this._$q.when(this); } var self = this; var onDomRemoved = self.config['onDomRemoved'] || angular.noop; var detachFn = function() { self._removeEventListeners(); // Remove the focus traps that we added earlier for keeping focus within // the panel. if (self._topFocusTrap && self._topFocusTrap.parentNode) { self._topFocusTrap.parentNode.removeChild(self._topFocusTrap); } if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) { self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap); } if (self._restoreCache.classes) { self.panelEl[0].className = self._restoreCache.classes; } // Either restore the saved styles or clear the ones set by mdPanel. self.panelEl[0].style.cssText = self._restoreCache.styles || ''; self._compilerCleanup(); self.panelContainer.remove(); self.isAttached = false; return self._$q.when(self); }; if (this._restoreScroll) { this._restoreScroll(); this._restoreScroll = null; } return this._$q(function(resolve, reject) { var done = self._done(resolve, self); self._$q.all([ detachFn(), self._backdropRef ? self._backdropRef.detach() : true ]).then(onDomRemoved) .then(done) .catch(reject); }); }; /** * Destroys the panel. The Panel cannot be opened again after this. */ MdPanelRef.prototype.destroy = function() { var self = this; if (this.config.groupName) { angular.forEach(this.config.groupName, function(group) { self.removeFromGroup(group); }); } this.config.scope.$destroy(); this.config.locals = null; this._interceptors = null; }; /** * Shows the panel. * @returns {!angular.$q.Promise} A promise that is resolved when * the panel has shown and animations finish. */ MdPanelRef.prototype.show = function() { if (!this.panelContainer) { return this._$q(function(resolve, reject) { reject('mdPanel: Panel does not exist yet. Call open() or attach().'); }); } if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { return this._$q.when(this); } var self = this; var animatePromise = function() { self.panelContainer.removeClass(MD_PANEL_HIDDEN); return self._animateOpen(); }; return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var onOpenComplete = self.config['onOpenComplete'] || angular.noop; var addToGroupOpen = function() { if (self.config.groupName) { angular.forEach(self.config.groupName, function(group) { self._$mdPanel._groups[group].openPanels.push(self); }); } }; self._$q.all([ self._backdropRef ? self._backdropRef.show() : self, animatePromise().then(function() { self._focusOnOpen(); }, reject) ]).then(onOpenComplete) .then(addToGroupOpen) .then(done) .catch(reject); }); }; /** * Hides the panel. * @returns {!angular.$q.Promise} A promise that is resolved when * the panel has hidden and animations finish. */ MdPanelRef.prototype.hide = function() { if (!this.panelContainer) { return this._$q(function(resolve, reject) { reject('mdPanel: Panel does not exist yet. Call open() or attach().'); }); } if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { return this._$q.when(this); } var self = this; return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var onRemoving = self.config['onRemoving'] || angular.noop; var hidePanel = function() { self.panelContainer.addClass(MD_PANEL_HIDDEN); }; var removeFromGroupOpen = function() { if (self.config.groupName) { var group, index; angular.forEach(self.config.groupName, function(group) { group = self._$mdPanel._groups[group]; index = group.openPanels.indexOf(self); if (index > -1) { group.openPanels.splice(index, 1); } }); } }; var focusOnOrigin = function() { var origin = self.config['origin']; if (origin) { getElement(origin).focus(); } }; self._$q.all([ self._backdropRef ? self._backdropRef.hide() : self, self._animateClose() .then(onRemoving) .then(hidePanel) .then(removeFromGroupOpen) .then(focusOnOrigin) .catch(reject) ]).then(done, reject); }); }; /** * Add a class to the panel. DO NOT use this to hide/show the panel. * @deprecated * This method is in the process of being deprecated in favor of using the panel * and container JQLite elements that are referenced in the MdPanelRef object. * Full deprecation is scheduled for material 1.2. * * @param {string} newClass Class to be added. * @param {boolean} toElement Whether or not to add the class to the panel * element instead of the container. */ MdPanelRef.prototype.addClass = function(newClass, toElement) { this._$log.warn( 'mdPanel: The addClass method is in the process of being deprecated. ' + 'Full deprecation is scheduled for the AngularJS Material 1.2 release. ' + 'To achieve the same results, use the panelContainer or panelEl ' + 'JQLite elements that are referenced in MdPanelRef.'); if (!this.panelContainer) { throw new Error( 'mdPanel: Panel does not exist yet. Call open() or attach().'); } if (!toElement && !this.panelContainer.hasClass(newClass)) { this.panelContainer.addClass(newClass); } else if (toElement && !this.panelEl.hasClass(newClass)) { this.panelEl.addClass(newClass); } }; /** * Remove a class from the panel. DO NOT use this to hide/show the panel. * @deprecated * This method is in the process of being deprecated in favor of using the panel * and container JQLite elements that are referenced in the MdPanelRef object. * Full deprecation is scheduled for material 1.2. * * @param {string} oldClass Class to be removed. * @param {boolean} fromElement Whether or not to remove the class from the * panel element instead of the container. */ MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { this._$log.warn( 'mdPanel: The removeClass method is in the process of being deprecated. ' + 'Full deprecation is scheduled for the AngularJS Material 1.2 release. ' + 'To achieve the same results, use the panelContainer or panelEl ' + 'JQLite elements that are referenced in MdPanelRef.'); if (!this.panelContainer) { throw new Error( 'mdPanel: Panel does not exist yet. Call open() or attach().'); } if (!fromElement && this.panelContainer.hasClass(oldClass)) { this.panelContainer.removeClass(oldClass); } else if (fromElement && this.panelEl.hasClass(oldClass)) { this.panelEl.removeClass(oldClass); } }; /** * Toggle a class on the panel. DO NOT use this to hide/show the panel. * @deprecated * This method is in the process of being deprecated in favor of using the panel * and container JQLite elements that are referenced in the MdPanelRef object. * Full deprecation is scheduled for material 1.2. * * @param {string} toggleClass The class to toggle. * @param {boolean} onElement Whether or not to toggle the class on the panel * element instead of the container. */ MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) { this._$log.warn( 'mdPanel: The toggleClass method is in the process of being deprecated. ' + 'Full deprecation is scheduled for the AngularJS Material 1.2 release. ' + 'To achieve the same results, use the panelContainer or panelEl ' + 'JQLite elements that are referenced in MdPanelRef.'); if (!this.panelContainer) { throw new Error( 'mdPanel: Panel does not exist yet. Call open() or attach().'); } if (!onElement) { this.panelContainer.toggleClass(toggleClass); } else { this.panelEl.toggleClass(toggleClass); } }; /** * Compiles the panel, according to the passed in config and appends it to * the DOM. Helps normalize differences in the compilation process between * using a string template and a content element. * @returns {!angular.$q.Promise} Promise that is resolved when * the element has been compiled and added to the DOM. * @private */ MdPanelRef.prototype._compile = function() { var self = this; // Compile the element via $mdCompiler. Note that when using a // contentElement, the element isn't actually being compiled, rather the // compiler saves it's place in the DOM and provides a way of restoring it. return self._$mdCompiler.compile(self.config).then(function(compileData) { var config = self.config; if (config.contentElement) { var panelEl = compileData.element; // Since mdPanel modifies the inline styles and CSS classes, we need // to save them in order to be able to restore on close. self._restoreCache.styles = panelEl[0].style.cssText; self._restoreCache.classes = panelEl[0].className; self.panelContainer = self._$mdPanel._wrapContentElement(panelEl); self.panelEl = panelEl; } else { self.panelContainer = compileData.link(config['scope']); self.panelEl = angular.element( self.panelContainer[0].querySelector('.md-panel') ); } // Save a reference to the cleanup function from the compiler. self._compilerCleanup = compileData.cleanup; // Attach the panel to the proper place in the DOM. getElement(self.config['attachTo']).append(self.panelContainer); return self; }); }; /** * Creates a panel and adds it to the dom. * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * created. * @private */ MdPanelRef.prototype._createPanel = function() { var self = this; return this._$q(function(resolve, reject) { if (!self.config.locals) { self.config.locals = {}; } self.config.locals.mdPanelRef = self; self._compile().then(function() { if (self.config['disableParentScroll']) { self._restoreScroll = self._$mdUtil.disableScrollAround( null, self.panelContainer, { disableScrollMask: true } ); } // Add a custom CSS class to the panel element. if (self.config['panelClass']) { self.panelEl.addClass(self.config['panelClass']); } // Handle click and touch events for the panel container. if (self.config['propagateContainerEvents']) { self.panelContainer.css('pointer-events', 'none'); self.panelEl.css('pointer-events', 'all'); } // Panel may be outside the $rootElement, tell ngAnimate to animate // regardless. if (self._$animate.pin) { self._$animate.pin( self.panelContainer, getElement(self.config['attachTo']) ); } self._configureTrapFocus(); self._addStyles().then(function() { resolve(self); }, reject); }, reject); }); }; /** * Adds the styles for the panel, such as positioning and z-index. Also, * themes the panel element and panel container using `$mdTheming`. * @returns {!angular.$q.Promise} * @private */ MdPanelRef.prototype._addStyles = function() { var self = this; return this._$q(function(resolve) { self.panelContainer.css('z-index', self.config['zIndex']); self.panelEl.css('z-index', self.config['zIndex'] + 1); var hideAndResolve = function() { // Theme the element and container. self._setTheming(); // Remove offscreen class and add hidden class. self.panelEl.removeClass('_md-panel-offscreen'); self.panelContainer.addClass(MD_PANEL_HIDDEN); resolve(self); }; if (self.config['fullscreen']) { self.panelEl.addClass('_md-panel-fullscreen'); hideAndResolve(); return; // Don't setup positioning. } var positionConfig = self.config['position']; if (!positionConfig) { hideAndResolve(); return; // Don't setup positioning. } // Wait for angular to finish processing the template self._$rootScope['$$postDigest'](function() { // Position it correctly. This is necessary so that the panel will have a // defined height and width. self._updatePosition(true); // Theme the element and container. self._setTheming(); resolve(self); }); }); }; /** * Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`. * @private */ MdPanelRef.prototype._setTheming = function() { this._$mdTheming(this.panelEl); this._$mdTheming(this.panelContainer); }; /** * Updates the position configuration of a panel * @param {!MdPanelPosition} position */ MdPanelRef.prototype.updatePosition = function(position) { if (!this.panelContainer) { throw new Error( 'mdPanel: Panel does not exist yet. Call open() or attach().'); } this.config['position'] = position; this._updatePosition(); }; /** * Calculates and updates the position of the panel. * @param {boolean=} init * @private */ MdPanelRef.prototype._updatePosition = function(init) { var positionConfig = this.config['position']; if (positionConfig) { positionConfig._setPanelPosition(this.panelEl); // Hide the panel now that position is known. if (init) { this.panelEl.removeClass('_md-panel-offscreen'); this.panelContainer.addClass(MD_PANEL_HIDDEN); } this.panelEl.css( MdPanelPosition.absPosition.TOP, positionConfig.getTop() ); this.panelEl.css( MdPanelPosition.absPosition.BOTTOM, positionConfig.getBottom() ); this.panelEl.css( MdPanelPosition.absPosition.LEFT, positionConfig.getLeft() ); this.panelEl.css( MdPanelPosition.absPosition.RIGHT, positionConfig.getRight() ); } }; /** * Focuses on the panel or the first focus target. * @private */ MdPanelRef.prototype._focusOnOpen = function() { if (this.config['focusOnOpen']) { // Wait for the template to finish rendering to guarantee md-autofocus has // finished adding the class md-autofocus, otherwise the focusable element // isn't available to focus. var self = this; this._$rootScope['$$postDigest'](function() { var target = self._$mdUtil.findFocusTarget(self.panelEl) || self.panelEl; target.focus(); }); } }; /** * Shows the backdrop. * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop * is created and attached. * @private */ MdPanelRef.prototype._createBackdrop = function() { if (this.config.hasBackdrop) { if (!this._backdropRef) { var backdropAnimation = this._$mdPanel.newPanelAnimation() .openFrom(this.config.attachTo) .withAnimation({ open: '_md-opaque-enter', close: '_md-opaque-leave' }); if (this.config.animation) { backdropAnimation.duration(this.config.animation._rawDuration); } var backdropConfig = { animation: backdropAnimation, attachTo: this.config.attachTo, focusOnOpen: false, panelClass: '_md-panel-backdrop', zIndex: this.config.zIndex - 1 }; this._backdropRef = this._$mdPanel.create(backdropConfig); } if (!this._backdropRef.isAttached) { return this._backdropRef.attach(); } } }; /** * Listen for escape keys and outside clicks to auto close. * @private */ MdPanelRef.prototype._addEventListeners = function() { this._configureEscapeToClose(); this._configureClickOutsideToClose(); this._configureScrollListener(); }; /** * Remove event listeners added in _addEventListeners. * @private */ MdPanelRef.prototype._removeEventListeners = function() { this._removeListeners && this._removeListeners.forEach(function(removeFn) { removeFn(); }); this._removeListeners = []; }; /** * Setup the escapeToClose event listeners. * @private */ MdPanelRef.prototype._configureEscapeToClose = function() { if (this.config['escapeToClose']) { var parentTarget = getElement(this.config['attachTo']); var self = this; var keyHandlerFn = function(ev) { if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) { ev.stopPropagation(); ev.preventDefault(); self.close(MdPanelRef.closeReasons.ESCAPE); } }; // Add keydown listeners this.panelContainer.on('keydown', keyHandlerFn); parentTarget.on('keydown', keyHandlerFn); // Queue remove listeners function this._removeListeners.push(function() { self.panelContainer.off('keydown', keyHandlerFn); parentTarget.off('keydown', keyHandlerFn); }); } }; /** * Setup the clickOutsideToClose event listeners. * @private */ MdPanelRef.prototype._configureClickOutsideToClose = function() { if (this.config['clickOutsideToClose']) { var target = this.config['propagateContainerEvents'] ? angular.element(document.body) : this.panelContainer; var sourceEl; // Keep track of the element on which the mouse originally went down // so that we can only close the backdrop when the 'click' started on it. // A simple 'click' handler does not work, it sets the target object as the // element the mouse went down on. var mousedownHandler = function(ev) { sourceEl = ev.target; }; // We check if our original element and the target is the backdrop // because if the original was the backdrop and the target was inside the // panel we don't want to panel to close. var self = this; var mouseupHandler = function(ev) { if (self.config['propagateContainerEvents']) { // We check if the sourceEl of the event is the panel element or one // of it's children. If it is not, then close the panel. if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) { self.close(); } } else if (sourceEl === target[0] && ev.target === target[0]) { ev.stopPropagation(); ev.preventDefault(); self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE); } }; // Add listeners target.on('mousedown', mousedownHandler); target.on('mouseup', mouseupHandler); // Queue remove listeners function this._removeListeners.push(function() { target.off('mousedown', mousedownHandler); target.off('mouseup', mouseupHandler); }); } }; /** * Configures the listeners for updating the panel position on scroll. * @private */ MdPanelRef.prototype._configureScrollListener = function() { // No need to bind the event if scrolling is disabled. if (!this.config['disableParentScroll']) { var updatePosition = angular.bind(this, this._updatePosition); var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition); var self = this; var onScroll = function() { debouncedUpdatePosition(); }; // Add listeners. this._$window.addEventListener('scroll', onScroll, true); // Queue remove listeners function. this._removeListeners.push(function() { self._$window.removeEventListener('scroll', onScroll, true); }); } }; /** * Setup the focus traps. These traps will wrap focus when tabbing past the * panel. When shift-tabbing, the focus will stick in place. * @private */ MdPanelRef.prototype._configureTrapFocus = function() { // Focus doesn't remain inside of the panel without this. this.panelEl.attr('tabIndex', '-1'); if (this.config['trapFocus']) { var element = this.panelEl; // Set up elements before and after the panel to capture focus and // redirect back into the panel. this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; // When focus is about to move out of the panel, we want to intercept it // and redirect it back to the panel element. var focusHandler = function() { element.focus(); }; this._topFocusTrap.addEventListener('focus', focusHandler); this._bottomFocusTrap.addEventListener('focus', focusHandler); // Queue remove listeners function this._removeListeners.push(this._simpleBind(function() { this._topFocusTrap.removeEventListener('focus', focusHandler); this._bottomFocusTrap.removeEventListener('focus', focusHandler); }, this)); // The top focus trap inserted immediately before the md-panel element (as // a sibling). The bottom focus trap inserted immediately after the // md-panel element (as a sibling). element[0].parentNode.insertBefore(this._topFocusTrap, element[0]); element.after(this._bottomFocusTrap); } }; /** * Updates the animation of a panel. * @param {!MdPanelAnimation} animation */ MdPanelRef.prototype.updateAnimation = function(animation) { this.config['animation'] = animation; if (this._backdropRef) { this._backdropRef.config.animation.duration(animation._rawDuration); } }; /** * Animate the panel opening. * @returns {!angular.$q.Promise} A promise that is resolved when the panel has * animated open. * @private */ MdPanelRef.prototype._animateOpen = function() { this.panelContainer.addClass('md-panel-is-showing'); var animationConfig = this.config['animation']; if (!animationConfig) { // Promise is in progress, return it. this.panelContainer.addClass('_md-panel-shown'); return this._$q.when(this); } var self = this; return this._$q(function(resolve) { var done = self._done(resolve, self); var warnAndOpen = function() { self._$log.warn( 'mdPanel: MdPanel Animations failed. ' + 'Showing panel without animating.'); done(); }; animationConfig.animateOpen(self.panelEl) .then(done, warnAndOpen); }); }; /** * Animate the panel closing. * @returns {!angular.$q.Promise} A promise that is resolved when the panel has * animated closed. * @private */ MdPanelRef.prototype._animateClose = function() { var animationConfig = this.config['animation']; if (!animationConfig) { this.panelContainer.removeClass('md-panel-is-showing'); this.panelContainer.removeClass('_md-panel-shown'); return this._$q.when(this); } var self = this; return this._$q(function(resolve) { var done = function() { self.panelContainer.removeClass('md-panel-is-showing'); resolve(self); }; var warnAndClose = function() { self._$log.warn( 'mdPanel: MdPanel Animations failed. ' + 'Hiding panel without animating.'); done(); }; animationConfig.animateClose(self.panelEl) .then(done, warnAndClose); }); }; /** * Registers a interceptor with the panel. The callback should return a promise, * which will allow the action to continue when it gets resolved, or will * prevent an action if it is rejected. * @param {string} type Type of interceptor. * @param {!angular.$q.Promise} callback Callback to be registered. * @returns {!MdPanelRef} */ MdPanelRef.prototype.registerInterceptor = function(type, callback) { var error = null; if (!angular.isString(type)) { error = 'Interceptor type must be a string, instead got ' + typeof type; } else if (!angular.isFunction(callback)) { error = 'Interceptor callback must be a function, instead got ' + typeof callback; } if (error) { throw new Error('MdPanel: ' + error); } var interceptors = this._interceptors[type] = this._interceptors[type] || []; if (interceptors.indexOf(callback) === -1) { interceptors.push(callback); } return this; }; /** * Removes a registered interceptor. * @param {string} type Type of interceptor to be removed. * @param {Function} callback Interceptor to be removed. * @returns {!MdPanelRef} */ MdPanelRef.prototype.removeInterceptor = function(type, callback) { var index = this._interceptors[type] ? this._interceptors[type].indexOf(callback) : -1; if (index > -1) { this._interceptors[type].splice(index, 1); } return this; }; /** * Removes all interceptors. * @param {string=} type Type of interceptors to be removed. * If ommited, all interceptors types will be removed. * @returns {!MdPanelRef} */ MdPanelRef.prototype.removeAllInterceptors = function(type) { if (type) { this._interceptors[type] = []; } else { this._interceptors = Object.create(null); } return this; }; /** * Invokes all the interceptors of a certain type sequantially in * reverse order. Works in a similar way to `$q.all`, except it * respects the order of the functions. * @param {string} type Type of interceptors to be invoked. * @returns {!angular.$q.Promise} * @private */ MdPanelRef.prototype._callInterceptors = function(type) { var self = this; var $q = self._$q; var interceptors = self._interceptors && self._interceptors[type] || []; return interceptors.reduceRight(function(promise, interceptor) { var isPromiseLike = interceptor && angular.isFunction(interceptor.then); var response = isPromiseLike ? interceptor : null; /** * For interceptors to reject/cancel subsequent portions of the chain, simply * return a `$q.reject()` */ return promise.then(function() { if (!response) { try { response = interceptor(self); } catch(e) { response = $q.reject(e); } } return response; }); }, $q.resolve(self)); }; /** * Faster, more basic than angular.bind * http://jsperf.com/angular-bind-vs-custom-vs-native * @param {function} callback * @param {!Object} self * @return {function} Callback function with a bound self. */ MdPanelRef.prototype._simpleBind = function(callback, self) { return function(value) { return callback.apply(self, value); }; }; /** * @param {function} callback * @param {!Object} self * @return {function} Callback function with a self param. */ MdPanelRef.prototype._done = function(callback, self) { return function() { callback(self); }; }; /** * Adds a panel to a group if the panel does not exist within the group already. * A panel can only exist within a single group. * @param {string} groupName The name of the group. */ MdPanelRef.prototype.addToGroup = function(groupName) { if (!this._$mdPanel._groups[groupName]) { this._$mdPanel.newPanelGroup(groupName); } var group = this._$mdPanel._groups[groupName]; var index = group.panels.indexOf(this); if (index < 0) { group.panels.push(this); } }; /** * Removes a panel from a group if the panel exists within that group. The group * must be created ahead of time. * @param {string} groupName The name of the group. */ MdPanelRef.prototype.removeFromGroup = function(groupName) { if (!this._$mdPanel._groups[groupName]) { throw new Error('mdPanel: The group ' + groupName + ' does not exist.'); } var group = this._$mdPanel._groups[groupName]; var index = group.panels.indexOf(this); if (index > -1) { group.panels.splice(index, 1); } }; /** * Possible default closeReasons for the close function. * @enum {string} */ MdPanelRef.closeReasons = { CLICK_OUTSIDE: 'clickOutsideToClose', ESCAPE: 'escapeToClose', }; /***************************************************************************** * MdPanelPosition * *****************************************************************************/ /** * Position configuration object. To use, create an MdPanelPosition with the * desired properties, then pass the object as part of $mdPanel creation. * * Example: * * var panelPosition = new MdPanelPosition() * .relativeTo(myButtonEl) * .addPanelPosition( * $mdPanel.xPosition.CENTER, * $mdPanel.yPosition.ALIGN_TOPS * ); * * $mdPanel.create({ * position: panelPosition * }); * * @param {!angular.$injector} $injector * @final @constructor */ function MdPanelPosition($injector) { /** @private @const {!angular.$window} */ this._$window = $injector.get('$window'); /** @private {boolean} */ this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl'; /** @private @const {!angular.$mdConstant} */ this._$mdConstant = $injector.get('$mdConstant'); /** @private {boolean} */ this._absolute = false; /** @private {!angular.JQLite} */ this._relativeToEl; /** @private {string} */ this._top = ''; /** @private {string} */ this._bottom = ''; /** @private {string} */ this._left = ''; /** @private {string} */ this._right = ''; /** @private {!Array} */ this._translateX = []; /** @private {!Array} */ this._translateY = []; /** @private {!Array<{x:string, y:string}>} */ this._positions = []; /** @private {?{x:string, y:string}} */ this._actualPosition; } /** * Possible values of xPosition. * @enum {string} */ MdPanelPosition.xPosition = { CENTER: 'center', ALIGN_START: 'align-start', ALIGN_END: 'align-end', OFFSET_START: 'offset-start', OFFSET_END: 'offset-end' }; /** * Possible values of yPosition. * @enum {string} */ MdPanelPosition.yPosition = { CENTER: 'center', ALIGN_TOPS: 'align-tops', ALIGN_BOTTOMS: 'align-bottoms', ABOVE: 'above', BELOW: 'below' }; /** * Possible values of absolute position. * @enum {string} */ MdPanelPosition.absPosition = { TOP: 'top', RIGHT: 'right', BOTTOM: 'bottom', LEFT: 'left' }; /** * Margin between the edges of a panel and the viewport. * @const {number} */ MdPanelPosition.viewportMargin = 8; /** * Sets absolute positioning for the panel. * @return {!MdPanelPosition} */ MdPanelPosition.prototype.absolute = function() { this._absolute = true; return this; }; /** * Sets the value of a position for the panel. Clears any previously set * position. * @param {string} position Position to set * @param {string=} value Value of the position. Defaults to '0'. * @returns {!MdPanelPosition} * @private */ MdPanelPosition.prototype._setPosition = function(position, value) { if (position === MdPanelPosition.absPosition.RIGHT || position === MdPanelPosition.absPosition.LEFT) { this._left = this._right = ''; } else if ( position === MdPanelPosition.absPosition.BOTTOM || position === MdPanelPosition.absPosition.TOP) { this._top = this._bottom = ''; } else { var positions = Object.keys(MdPanelPosition.absPosition).join() .toLowerCase(); throw new Error('mdPanel: Position must be one of ' + positions + '.'); } this['_' + position] = angular.isString(value) ? value : '0'; return this; }; /** * Sets the value of `top` for the panel. Clears any previously set vertical * position. * @param {string=} top Value of `top`. Defaults to '0'. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.top = function(top) { return this._setPosition(MdPanelPosition.absPosition.TOP, top); }; /** * Sets the value of `bottom` for the panel. Clears any previously set vertical * position. * @param {string=} bottom Value of `bottom`. Defaults to '0'. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.bottom = function(bottom) { return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom); }; /** * Sets the panel to the start of the page - `left` if `ltr` or `right` for * `rtl`. Clears any previously set horizontal position. * @param {string=} start Value of position. Defaults to '0'. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.start = function(start) { var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT; return this._setPosition(position, start); }; /** * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. * Clears any previously set horizontal position. * @param {string=} end Value of position. Defaults to '0'. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.end = function(end) { var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT; return this._setPosition(position, end); }; /** * Sets the value of `left` for the panel. Clears any previously set * horizontal position. * @param {string=} left Value of `left`. Defaults to '0'. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.left = function(left) { return this._setPosition(MdPanelPosition.absPosition.LEFT, left); }; /** * Sets the value of `right` for the panel. Clears any previously set * horizontal position. * @param {string=} right Value of `right`. Defaults to '0'. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.right = function(right) { return this._setPosition(MdPanelPosition.absPosition.RIGHT, right); }; /** * Centers the panel horizontally in the viewport. Clears any previously set * horizontal position. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.centerHorizontally = function() { this._left = '50%'; this._right = ''; this._translateX = ['-50%']; return this; }; /** * Centers the panel vertically in the viewport. Clears any previously set * vertical position. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.centerVertically = function() { this._top = '50%'; this._bottom = ''; this._translateY = ['-50%']; return this; }; /** * Centers the panel horizontally and vertically in the viewport. This is * equivalent to calling both `centerHorizontally` and `centerVertically`. * Clears any previously set horizontal and vertical positions. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.center = function() { return this.centerHorizontally().centerVertically(); }; /** * Sets element for relative positioning. * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, * or angular element to set the panel relative to. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.relativeTo = function(element) { this._absolute = false; this._relativeToEl = getElement(element); return this; }; /** * Sets the x and y positions for the panel relative to another element. * @param {string} xPosition must be one of the MdPanelPosition.xPosition * values. * @param {string} yPosition must be one of the MdPanelPosition.yPosition * values. * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { if (!this._relativeToEl) { throw new Error('mdPanel: addPanelPosition can only be used with ' + 'relative positioning. Set relativeTo first.'); } this._validateXPosition(xPosition); this._validateYPosition(yPosition); this._positions.push({ x: xPosition, y: yPosition, }); return this; }; /** * Ensures that yPosition is a valid position name. Throw an exception if not. * @param {string} yPosition */ MdPanelPosition.prototype._validateYPosition = function(yPosition) { // empty is ok if (yPosition == null) { return; } var positionKeys = Object.keys(MdPanelPosition.yPosition); var positionValues = []; for (var key, i = 0; key = positionKeys[i]; i++) { var position = MdPanelPosition.yPosition[key]; positionValues.push(position); if (position === yPosition) { return; } } throw new Error('mdPanel: Panel y position only accepts the following ' + 'values:\n' + positionValues.join(' | ')); }; /** * Ensures that xPosition is a valid position name. Throw an exception if not. * @param {string} xPosition */ MdPanelPosition.prototype._validateXPosition = function(xPosition) { // empty is ok if (xPosition == null) { return; } var positionKeys = Object.keys(MdPanelPosition.xPosition); var positionValues = []; for (var key, i = 0; key = positionKeys[i]; i++) { var position = MdPanelPosition.xPosition[key]; positionValues.push(position); if (position === xPosition) { return; } } throw new Error('mdPanel: Panel x Position only accepts the following ' + 'values:\n' + positionValues.join(' | ')); }; /** * Sets the value of the offset in the x-direction. This will add to any * previously set offsets. * @param {string|function(MdPanelPosition): string} offsetX * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.withOffsetX = function(offsetX) { this._translateX.push(offsetX); return this; }; /** * Sets the value of the offset in the y-direction. This will add to any * previously set offsets. * @param {string|function(MdPanelPosition): string} offsetY * @returns {!MdPanelPosition} */ MdPanelPosition.prototype.withOffsetY = function(offsetY) { this._translateY.push(offsetY); return this; }; /** * Gets the value of `top` for the panel. * @returns {string} */ MdPanelPosition.prototype.getTop = function() { return this._top; }; /** * Gets the value of `bottom` for the panel. * @returns {string} */ MdPanelPosition.prototype.getBottom = function() { return this._bottom; }; /** * Gets the value of `left` for the panel. * @returns {string} */ MdPanelPosition.prototype.getLeft = function() { return this._left; }; /** * Gets the value of `right` for the panel. * @returns {string} */ MdPanelPosition.prototype.getRight = function() { return this._right; }; /** * Gets the value of `transform` for the panel. * @returns {string} */ MdPanelPosition.prototype.getTransform = function() { var translateX = this._reduceTranslateValues('translateX', this._translateX); var translateY = this._reduceTranslateValues('translateY', this._translateY); // It's important to trim the result, because the browser will ignore the set // operation if the string contains only whitespace. return (translateX + ' ' + translateY).trim(); }; /** * Sets the `transform` value for a panel element. * @param {!angular.JQLite} panelEl * @returns {!angular.JQLite} * @private */ MdPanelPosition.prototype._setTransform = function(panelEl) { return panelEl.css(this._$mdConstant.CSS.TRANSFORM, this.getTransform()); }; /** * True if the panel is completely on-screen with this positioning; false * otherwise. * @param {!angular.JQLite} panelEl * @return {boolean} * @private */ MdPanelPosition.prototype._isOnscreen = function(panelEl) { // this works because we always use fixed positioning for the panel, // which is relative to the viewport. var left = parseInt(this.getLeft()); var top = parseInt(this.getTop()); if (this._translateX.length || this._translateY.length) { var prefixedTransform = this._$mdConstant.CSS.TRANSFORM; var offsets = getComputedTranslations(panelEl, prefixedTransform); left += offsets.x; top += offsets.y; } var right = left + panelEl[0].offsetWidth; var bottom = top + panelEl[0].offsetHeight; return (left >= 0) && (top >= 0) && (bottom <= this._$window.innerHeight) && (right <= this._$window.innerWidth); }; /** * Gets the first x/y position that can fit on-screen. * @returns {{x: string, y: string}} */ MdPanelPosition.prototype.getActualPosition = function() { return this._actualPosition; }; /** * Reduces a list of translate values to a string that can be used within * transform. * @param {string} translateFn * @param {!Array} values * @returns {string} * @private */ MdPanelPosition.prototype._reduceTranslateValues = function(translateFn, values) { return values.map(function(translation) { // TODO(crisbeto): this should add the units after #9609 is merged. var translationValue = angular.isFunction(translation) ? translation(this) : translation; return translateFn + '(' + translationValue + ')'; }, this).join(' '); }; /** * Sets the panel position based on the created panel element and best x/y * positioning. * @param {!angular.JQLite} panelEl * @private */ MdPanelPosition.prototype._setPanelPosition = function(panelEl) { // Remove the "position adjusted" class in case it has been added before. panelEl.removeClass('_md-panel-position-adjusted'); // Only calculate the position if necessary. if (this._absolute) { this._setTransform(panelEl); return; } if (this._actualPosition) { this._calculatePanelPosition(panelEl, this._actualPosition); this._setTransform(panelEl); this._constrainToViewport(panelEl); return; } for (var i = 0; i < this._positions.length; i++) { this._actualPosition = this._positions[i]; this._calculatePanelPosition(panelEl, this._actualPosition); this._setTransform(panelEl); if (this._isOnscreen(panelEl)) { return; } } this._constrainToViewport(panelEl); }; /** * Constrains a panel's position to the viewport. * @param {!angular.JQLite} panelEl * @private */ MdPanelPosition.prototype._constrainToViewport = function(panelEl) { var margin = MdPanelPosition.viewportMargin; var initialTop = this._top; var initialLeft = this._left; if (this.getTop()) { var top = parseInt(this.getTop()); var bottom = panelEl[0].offsetHeight + top; var viewportHeight = this._$window.innerHeight; if (top < margin) { this._top = margin + 'px'; } else if (bottom > viewportHeight) { this._top = top - (bottom - viewportHeight + margin) + 'px'; } } if (this.getLeft()) { var left = parseInt(this.getLeft()); var right = panelEl[0].offsetWidth + left; var viewportWidth = this._$window.innerWidth; if (left < margin) { this._left = margin + 'px'; } else if (right > viewportWidth) { this._left = left - (right - viewportWidth + margin) + 'px'; } } // Class that can be used to re-style the panel if it was repositioned. panelEl.toggleClass( '_md-panel-position-adjusted', this._top !== initialTop || this._left !== initialLeft ); }; /** * Switches between 'start' and 'end'. * @param {string} position Horizontal position of the panel * @returns {string} Reversed position * @private */ MdPanelPosition.prototype._reverseXPosition = function(position) { if (position === MdPanelPosition.xPosition.CENTER) { return position; } var start = 'start'; var end = 'end'; return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start); }; /** * Handles horizontal positioning in rtl or ltr environments. * @param {string} position Horizontal position of the panel * @returns {string} The correct position according the page direction * @private */ MdPanelPosition.prototype._bidi = function(position) { return this._isRTL ? this._reverseXPosition(position) : position; }; /** * Calculates the panel position based on the created panel element and the * provided positioning. * @param {!angular.JQLite} panelEl * @param {!{x:string, y:string}} position * @private */ MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) { var panelBounds = panelEl[0].getBoundingClientRect(); var panelWidth = panelBounds.width; var panelHeight = panelBounds.height; var targetBounds = this._relativeToEl[0].getBoundingClientRect(); var targetLeft = targetBounds.left; var targetRight = targetBounds.right; var targetWidth = targetBounds.width; switch (this._bidi(position.x)) { case MdPanelPosition.xPosition.OFFSET_START: this._left = targetLeft - panelWidth + 'px'; break; case MdPanelPosition.xPosition.ALIGN_END: this._left = targetRight - panelWidth + 'px'; break; case MdPanelPosition.xPosition.CENTER: var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth); this._left = left + 'px'; break; case MdPanelPosition.xPosition.ALIGN_START: this._left = targetLeft + 'px'; break; case MdPanelPosition.xPosition.OFFSET_END: this._left = targetRight + 'px'; break; } var targetTop = targetBounds.top; var targetBottom = targetBounds.bottom; var targetHeight = targetBounds.height; switch (position.y) { case MdPanelPosition.yPosition.ABOVE: this._top = targetTop - panelHeight + 'px'; break; case MdPanelPosition.yPosition.ALIGN_BOTTOMS: this._top = targetBottom - panelHeight + 'px'; break; case MdPanelPosition.yPosition.CENTER: var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight); this._top = top + 'px'; break; case MdPanelPosition.yPosition.ALIGN_TOPS: this._top = targetTop + 'px'; break; case MdPanelPosition.yPosition.BELOW: this._top = targetBottom + 'px'; break; } }; /***************************************************************************** * MdPanelAnimation * *****************************************************************************/ /** * Animation configuration object. To use, create an MdPanelAnimation with the * desired properties, then pass the object as part of $mdPanel creation. * * Example: * * var panelAnimation = new MdPanelAnimation() * .openFrom(myButtonEl) * .closeTo('.my-button') * .withAnimation($mdPanel.animation.SCALE); * * $mdPanel.create({ * animation: panelAnimation * }); * * @param {!angular.$injector} $injector * @final @constructor */ function MdPanelAnimation($injector) { /** @private @const {!angular.$mdUtil} */ this._$mdUtil = $injector.get('$mdUtil'); /** * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| * undefined} */ this._openFrom; /** * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| * undefined} */ this._closeTo; /** @private {string|{open: string, close: string}} */ this._animationClass = ''; /** @private {number} */ this._openDuration; /** @private {number} */ this._closeDuration; /** @private {number|{open: number, close: number}} */ this._rawDuration; } /** * Possible default animations. * @enum {string} */ MdPanelAnimation.animation = { SLIDE: 'md-panel-animate-slide', SCALE: 'md-panel-animate-scale', FADE: 'md-panel-animate-fade' }; /** * Specifies where to start the open animation. `openFrom` accepts a * click event object, query selector, DOM element, or a Rect object that * is used to determine the bounds. When passed a click event, the location * of the click will be used as the position to start the animation. * @param {string|!Element|!Event|{top: number, left: number}} openFrom * @returns {!MdPanelAnimation} */ MdPanelAnimation.prototype.openFrom = function(openFrom) { // Check if 'openFrom' is an Event. openFrom = openFrom.target ? openFrom.target : openFrom; this._openFrom = this._getPanelAnimationTarget(openFrom); if (!this._closeTo) { this._closeTo = this._openFrom; } return this; }; /** * Specifies where to animate the panel close. `closeTo` accepts a * query selector, DOM element, or a Rect object that is used to determine * the bounds. * @param {string|!Element|{top: number, left: number}} closeTo * @returns {!MdPanelAnimation} */ MdPanelAnimation.prototype.closeTo = function(closeTo) { this._closeTo = this._getPanelAnimationTarget(closeTo); return this; }; /** * Specifies the duration of the animation in milliseconds. * @param {number|{open: number, close: number}} duration * @returns {!MdPanelAnimation} */ MdPanelAnimation.prototype.duration = function(duration) { if (duration) { if (angular.isNumber(duration)) { this._openDuration = this._closeDuration = toSeconds(duration); } else if (angular.isObject(duration)) { this._openDuration = toSeconds(duration.open); this._closeDuration = toSeconds(duration.close); } } // Save the original value so it can be passed to the backdrop. this._rawDuration = duration; return this; function toSeconds(value) { if (angular.isNumber(value)) return value / 1000; } }; /** * Returns the element and bounds for the animation target. * @param {string|!Element|{top: number, left: number}} location * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} * @private */ MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { if (angular.isDefined(location.top) || angular.isDefined(location.left)) { return { element: undefined, bounds: { top: location.top || 0, left: location.left || 0 } }; } else { return this._getBoundingClientRect(getElement(location)); } }; /** * Specifies the animation class. * * There are several default animations that can be used: * (MdPanelAnimation.animation) * SLIDE: The panel slides in and out from the specified * elements. * SCALE: The panel scales in and out. * FADE: The panel fades in and out. * * @param {string|{open: string, close: string}} cssClass * @returns {!MdPanelAnimation} */ MdPanelAnimation.prototype.withAnimation = function(cssClass) { this._animationClass = cssClass; return this; }; /** * Animate the panel open. * @param {!angular.JQLite} panelEl * @returns {!angular.$q.Promise} A promise that is resolved when the open * animation is complete. */ MdPanelAnimation.prototype.animateOpen = function(panelEl) { var animator = this._$mdUtil.dom.animator; this._fixBounds(panelEl); var animationOptions = {}; // Include the panel transformations when calculating the animations. var panelTransform = panelEl[0].style.transform || ''; var openFrom = animator.toTransformCss(panelTransform); var openTo = animator.toTransformCss(panelTransform); switch (this._animationClass) { case MdPanelAnimation.animation.SLIDE: // Slide should start with opacity: 1. panelEl.css('opacity', '1'); animationOptions = { transitionInClass: '_md-panel-animate-enter' }; var openSlide = animator.calculateSlideToOrigin( panelEl, this._openFrom) || ''; openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform); break; case MdPanelAnimation.animation.SCALE: animationOptions = { transitionInClass: '_md-panel-animate-enter' }; var openScale = animator.calculateZoomToOrigin( panelEl, this._openFrom) || ''; openFrom = animator.toTransformCss(openScale + ' ' + panelTransform); break; case MdPanelAnimation.animation.FADE: animationOptions = { transitionInClass: '_md-panel-animate-enter' }; break; default: if (angular.isString(this._animationClass)) { animationOptions = { transitionInClass: this._animationClass }; } else { animationOptions = { transitionInClass: this._animationClass['open'], transitionOutClass: this._animationClass['close'], }; } } animationOptions.duration = this._openDuration; return animator .translate3d(panelEl, openFrom, openTo, animationOptions); }; /** * Animate the panel close. * @param {!angular.JQLite} panelEl * @returns {!angular.$q.Promise} A promise that resolves when the close * animation is complete. */ MdPanelAnimation.prototype.animateClose = function(panelEl) { var animator = this._$mdUtil.dom.animator; var reverseAnimationOptions = {}; // Include the panel transformations when calculating the animations. var panelTransform = panelEl[0].style.transform || ''; var closeFrom = animator.toTransformCss(panelTransform); var closeTo = animator.toTransformCss(panelTransform); switch (this._animationClass) { case MdPanelAnimation.animation.SLIDE: // Slide should start with opacity: 1. panelEl.css('opacity', '1'); reverseAnimationOptions = { transitionInClass: '_md-panel-animate-leave' }; var closeSlide = animator.calculateSlideToOrigin( panelEl, this._closeTo) || ''; closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform); break; case MdPanelAnimation.animation.SCALE: reverseAnimationOptions = { transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave' }; var closeScale = animator.calculateZoomToOrigin( panelEl, this._closeTo) || ''; closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform); break; case MdPanelAnimation.animation.FADE: reverseAnimationOptions = { transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave' }; break; default: if (angular.isString(this._animationClass)) { reverseAnimationOptions = { transitionOutClass: this._animationClass }; } else { reverseAnimationOptions = { transitionInClass: this._animationClass['close'], transitionOutClass: this._animationClass['open'] }; } } reverseAnimationOptions.duration = this._closeDuration; return animator .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions); }; /** * Set the height and width to match the panel if not provided. * @param {!angular.JQLite} panelEl * @private */ MdPanelAnimation.prototype._fixBounds = function(panelEl) { var panelWidth = panelEl[0].offsetWidth; var panelHeight = panelEl[0].offsetHeight; if (this._openFrom && this._openFrom.bounds.height == null) { this._openFrom.bounds.height = panelHeight; } if (this._openFrom && this._openFrom.bounds.width == null) { this._openFrom.bounds.width = panelWidth; } if (this._closeTo && this._closeTo.bounds.height == null) { this._closeTo.bounds.height = panelHeight; } if (this._closeTo && this._closeTo.bounds.width == null) { this._closeTo.bounds.width = panelWidth; } }; /** * Identify the bounding RECT for the target element. * @param {!angular.JQLite} element * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} * @private */ MdPanelAnimation.prototype._getBoundingClientRect = function(element) { if (element instanceof angular.element) { return { element: element, bounds: element[0].getBoundingClientRect() }; } }; /***************************************************************************** * Util Methods * *****************************************************************************/ /** * Returns the angular element associated with a css selector or element. * @param el {string|!angular.JQLite|!Element} * @returns {!angular.JQLite} */ function getElement(el) { var queryResult = angular.isString(el) ? document.querySelector(el) : el; return angular.element(queryResult); } /** * Gets the computed values for an element's translateX and translateY in px. * @param {!angular.JQLite|!Element} el * @param {string} property * @return {{x: number, y: number}} */ function getComputedTranslations(el, property) { // The transform being returned by `getComputedStyle` is in the format: // `matrix(a, b, c, d, translateX, translateY)` if defined and `none` // if the element doesn't have a transform. var transform = getComputedStyle(el[0] || el)[property]; var openIndex = transform.indexOf('('); var closeIndex = transform.lastIndexOf(')'); var output = { x: 0, y: 0 }; if (openIndex > -1 && closeIndex > -1) { var parsedValues = transform .substring(openIndex + 1, closeIndex) .split(', ') .slice(-2); output.x = parseInt(parsedValues[0]); output.y = parseInt(parsedValues[1]); } return output; } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.progressCircular * @description Module for a circular progressbar */ angular.module('material.components.progressCircular', ['material.core']); })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.progressLinear * @description Linear Progress module! */ MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; angular.module('material.components.progressLinear', [ 'material.core' ]) .directive('mdProgressLinear', MdProgressLinearDirective); /** * @ngdoc directive * @name mdProgressLinear * @module material.components.progressLinear * @restrict E * * @description * The linear progress directive is used to make loading content * in your app as delightful and painless as possible by minimizing * the amount of visual change a user sees before they can view * and interact with content. * * Each operation should only be represented by one activity indicator * For example: one refresh operation should not display both a * refresh bar and an activity circle. * * For operations where the percentage of the operation completed * can be determined, use a determinate indicator. They give users * a quick sense of how long an operation will take. * * For operations where the user is asked to wait a moment while * something finishes up, and it’s not necessary to expose what's * happening behind the scenes and how long it will take, use an * indeterminate indicator. * * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. * * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate` * will be auto-applied as the mode. * * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, * then `md-mode="determinate"` would be auto-injected instead. * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 * @param {boolean=} ng-disabled Determines whether to disable the progress element. * * @usage * * * * * * * * * * * */ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { var MODE_DETERMINATE = "determinate"; var MODE_INDETERMINATE = "indeterminate"; var MODE_BUFFER = "buffer"; var MODE_QUERY = "query"; var DISABLED_CLASS = "_md-progress-linear-disabled"; return { restrict: 'E', template: '
    ' + '
    ' + '
    ' + '
    ' + '
    ', compile: compile }; function compile(tElement, tAttrs, transclude) { tElement.attr('aria-valuemin', 0); tElement.attr('aria-valuemax', 100); tElement.attr('role', 'progressbar'); return postLink; } function postLink(scope, element, attr) { $mdTheming(element); var lastMode; var isDisabled = attr.hasOwnProperty('disabled'); var toVendorCSS = $mdUtil.dom.animator.toCss; var bar1 = angular.element(element[0].querySelector('.md-bar1')); var bar2 = angular.element(element[0].querySelector('.md-bar2')); var container = angular.element(element[0].querySelector('.md-container')); element .attr('md-mode', mode()) .toggleClass(DISABLED_CLASS, isDisabled); validateMode(); watchAttributes(); /** * Watch the value, md-buffer-value, and md-mode attributes */ function watchAttributes() { attr.$observe('value', function(value) { var percentValue = clamp(value); element.attr('aria-valuenow', percentValue); if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); }); attr.$observe('mdBufferValue', function(value) { animateIndicator(bar1, clamp(value)); }); attr.$observe('disabled', function(value) { if (value === true || value === false) { isDisabled = !!value; } else { isDisabled = angular.isDefined(value); } element.toggleClass(DISABLED_CLASS, isDisabled); container.toggleClass(lastMode, !isDisabled); }); attr.$observe('mdMode', function(mode) { if (lastMode) container.removeClass( lastMode ); switch( mode ) { case MODE_QUERY: case MODE_BUFFER: case MODE_DETERMINATE: case MODE_INDETERMINATE: container.addClass( lastMode = "md-mode-" + mode ); break; default: container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE ); break; } }); } /** * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified */ function validateMode() { if ( angular.isUndefined(attr.mdMode) ) { var hasValue = angular.isDefined(attr.value); var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; //$log.debug( $mdUtil.supplant(info, [mode]) ); element.attr("md-mode", mode); attr.mdMode = mode; } } /** * Is the md-mode a valid option? */ function mode() { var value = (attr.mdMode || "").trim(); if ( value ) { switch(value) { case MODE_DETERMINATE: case MODE_INDETERMINATE: case MODE_BUFFER: case MODE_QUERY: break; default: value = MODE_INDETERMINATE; break; } } return value; } /** * Manually set CSS to animate the Determinate indicator based on the specified * percentage value (0-100). */ function animateIndicator(target, value) { if ( isDisabled || !mode() ) return; var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); var styles = toVendorCSS({ transform : to }); angular.element(target).css( styles ); } } /** * Clamps the value to be between 0 and 100. * @param {number} value The value to clamp. * @returns {number} */ function clamp(value) { return Math.max(0, Math.min(value || 0, 100)); } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.radioButton * @description radioButton module! */ mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; angular.module('material.components.radioButton', [ 'material.core' ]) .directive('mdRadioGroup', mdRadioGroupDirective) .directive('mdRadioButton', mdRadioButtonDirective); /** * @ngdoc directive * @module material.components.radioButton * @name mdRadioGroup * * @restrict E * * @description * The `` directive identifies a grouping * container for the 1..n grouped radio buttons; specified using nested * `` tags. * * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) * the radio button is in the accent color by default. The primary color palette may be used with * the `md-primary` class. * * Note: `` and `` handle tabindex differently * than the native `` controls. Whereas the native controls * force the user to tab through all the radio buttons, `` * is focusable, and by default the ``s are not. * * @param {string} ng-model Assignable angular expression to data-bind to. * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @usage * * * * * * {{ d.label }} * * * * * * */ function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { RadioGroupController.prototype = createRadioGroupControllerProto(); return { restrict: 'E', controller: ['$element', RadioGroupController], require: ['mdRadioGroup', '?ngModel'], link: { pre: linkRadioGroup } }; function linkRadioGroup(scope, element, attr, ctrls) { element.addClass('_md'); // private md component indicator for styling $mdTheming(element); var rgCtrl = ctrls[0]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); rgCtrl.init(ngModelCtrl); scope.mouseActive = false; element .attr({ 'role': 'radiogroup', 'tabIndex': element.attr('tabindex') || '0' }) .on('keydown', keydownListener) .on('mousedown', function(event) { scope.mouseActive = true; $timeout(function() { scope.mouseActive = false; }, 100); }) .on('focus', function() { if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); } }) .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); }); /** * */ function setFocus() { if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } } /** * */ function keydownListener(ev) { var keyCode = ev.which || ev.keyCode; // Only listen to events that we originated ourselves // so that we don't trigger on things like arrow keys in // inputs. if (keyCode != $mdConstant.KEY_CODE.ENTER && ev.currentTarget != ev.target) { return; } switch (keyCode) { case $mdConstant.KEY_CODE.LEFT_ARROW: case $mdConstant.KEY_CODE.UP_ARROW: ev.preventDefault(); rgCtrl.selectPrevious(); setFocus(); break; case $mdConstant.KEY_CODE.RIGHT_ARROW: case $mdConstant.KEY_CODE.DOWN_ARROW: ev.preventDefault(); rgCtrl.selectNext(); setFocus(); break; case $mdConstant.KEY_CODE.ENTER: var form = angular.element($mdUtil.getClosest(element[0], 'form')); if (form.length > 0) { form.triggerHandler('submit'); } break; } } } function RadioGroupController($element) { this._radioButtonRenderFns = []; this.$element = $element; } function createRadioGroupControllerProto() { return { init: function(ngModelCtrl) { this._ngModelCtrl = ngModelCtrl; this._ngModelCtrl.$render = angular.bind(this, this.render); }, add: function(rbRender) { this._radioButtonRenderFns.push(rbRender); }, remove: function(rbRender) { var index = this._radioButtonRenderFns.indexOf(rbRender); if (index !== -1) { this._radioButtonRenderFns.splice(index, 1); } }, render: function() { this._radioButtonRenderFns.forEach(function(rbRender) { rbRender(); }); }, setViewValue: function(value, eventType) { this._ngModelCtrl.$setViewValue(value, eventType); // update the other radio buttons as well this.render(); }, getViewValue: function() { return this._ngModelCtrl.$viewValue; }, selectNext: function() { return changeSelectedButton(this.$element, 1); }, selectPrevious: function() { return changeSelectedButton(this.$element, -1); }, setActiveDescendant: function (radioId) { this.$element.attr('aria-activedescendant', radioId); }, isDisabled: function() { return this.$element[0].hasAttribute('disabled'); } }; } /** * Change the radio group's selected button by a given increment. * If no button is selected, select the first button. */ function changeSelectedButton(parent, increment) { // Coerce all child radio buttons into an array, then wrap then in an iterator var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); if (buttons.count()) { var validate = function (button) { // If disabled, then NOT valid return !angular.element(button).attr("disabled"); }; var selected = parent[0].querySelector('md-radio-button.md-checked'); var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); // Activate radioButton's click listener (triggerHandler won't create a real click event) angular.element(target).triggerHandler('click'); } } } /** * @ngdoc directive * @module material.components.radioButton * @name mdRadioButton * * @restrict E * * @description * The ``directive is the child directive required to be used within `` elements. * * While similar to the `` directive, * the `` directive provides ink effects, ARIA support, and * supports use within named radio groups. * * @param {string} ngValue AngularJS expression which sets the value to which the expression should * be set when selected. * @param {string} value The value to which the expression should be set when selected. * @param {string=} name Property name of the form under which the control is published. * @param {string=} aria-label Adds label to radio button for accessibility. * Defaults to radio button's text. If no text content is available, a warning will be logged. * * @usage * * * * Label 1 * * * * Green * * * * */ function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { var CHECKED_CSS = 'md-checked'; return { restrict: 'E', require: '^mdRadioGroup', transclude: true, template: '
    ' + '
    ' + '
    ' + '
    ' + '
    ', link: link }; function link(scope, element, attr, rgCtrl) { var lastChecked; $mdTheming(element); configureAria(element, scope); // ngAria overwrites the aria-checked inside a $watch for ngValue. // We should defer the initialization until all the watches have fired. // This can also be fixed by removing the `lastChecked` check, but that'll // cause more DOM manipulation on each digest. if (attr.ngValue) { $mdUtil.nextTick(initialize, false); } else { initialize(); } /** * Initializes the component. */ function initialize() { if (!rgCtrl) { throw 'RadioButton: No RadioGroupController could be found.'; } rgCtrl.add(render); attr.$observe('value', render); element .on('click', listener) .on('$destroy', function() { rgCtrl.remove(render); }); } /** * On click functionality. */ function listener(ev) { if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; scope.$apply(function() { rgCtrl.setViewValue(attr.value, ev && ev.type); }); } /** * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). * Update the `aria-activedescendant` attribute. */ function render() { var checked = rgCtrl.getViewValue() == attr.value; if (checked === lastChecked) return; if (element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') { // If the radioButton is inside a div, then add class so highlighting will work element.parent().toggleClass(CHECKED_CSS, checked); } if (checked) { rgCtrl.setActiveDescendant(element.attr('id')); } lastChecked = checked; element .attr('aria-checked', checked) .toggleClass(CHECKED_CSS, checked); } /** * Inject ARIA-specific attributes appropriate for each radio button */ function configureAria(element, scope){ element.attr({ id: attr.id || 'radio_' + $mdUtil.nextUid(), role: 'radio', 'aria-checked': 'false' }); $mdAria.expectWithText(element, 'aria-label'); } } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.select */ /*************************************************** ### TODO - POST RC1 ### - [ ] Abstract placement logic in $mdSelect service to $mdMenu service ***************************************************/ SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"]; SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"]; OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil", "$mdTheming"]; SelectProvider.$inject = ["$$interimElementProvider"]; var SELECT_EDGE_MARGIN = 8; var selectNextId = 0; var CHECKBOX_SELECTION_INDICATOR = angular.element('
    '); angular.module('material.components.select', [ 'material.core', 'material.components.backdrop' ]) .directive('mdSelect', SelectDirective) .directive('mdSelectMenu', SelectMenuDirective) .directive('mdOption', OptionDirective) .directive('mdOptgroup', OptgroupDirective) .directive('mdSelectHeader', SelectHeaderDirective) .provider('$mdSelect', SelectProvider); /** * @ngdoc directive * @name mdSelect * @restrict E * @module material.components.select * * @description Displays a select box, bound to an ng-model. * * When the select is required and uses a floating label, then the label will automatically contain * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute. * * By default, the select will display with an underline to match other form elements. This can be * disabled by applying the `md-no-underline` CSS class. * * ### Option Params * * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the * select and put it back in it's default state. You may supply this attribute on any option you * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not * defined. * * **Automatically Applied** * * - `` * - `` * - `` * - `` * - `` * * **NOT Automatically Applied** * * - `` * - `` * - `` * - `` (this evaluates to the string `"undefined"`) * - <md-option ng-value="{{someValueThatMightBeUndefined}}"> * * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this * attribute) since you may wish this to be your "Not Available" or "None" option. * * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`. * * @param {expression} ng-model The model! * @param {boolean=} multiple When set to true, allows for more than one option to be selected. The model is an array with the selected choices. * @param {expression=} md-on-close Expression to be evaluated when the select is closed. * @param {expression=} md-on-open Expression to be evaluated when opening the select. * Will hide the select options and show a spinner until the evaluated promise resolves. * @param {expression=} md-selected-text Expression to be evaluated that will return a string * to be displayed as a placeholder in the select input box when it is closed. The value * will be treated as *text* (not html). * @param {expression=} md-selected-html Expression to be evaluated that will return a string * to be displayed as a placeholder in the select input box when it is closed. The value * will be treated as *html*. The value must either be explicitly marked as trustedHtml or * the ngSanitize module must be loaded. * @param {string=} placeholder Placeholder hint text. * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the * floating label. **Note:** This attribute is only evaluated once; it is not watched. * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or * explicit label is present. * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` * element (for custom styling). * * @usage * With a placeholder (label and aria-label are added dynamically) * * * * {{ opt }} * * * * * With an explicit label * * * * * {{ opt }} * * * * * With a select-header * * When a developer needs to put more than just a text label in the * md-select-menu, they should use the md-select-header. * The user can put custom HTML inside of the header and style it to their liking. * One common use case of this would be a sticky search bar. * * When using the md-select-header the labels that would previously be added to the * OptGroupDirective are ignored. * * * * * * Neighborhoods - * * {{ opt }} * * * * * ## Selects and object equality * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles * equality. Consider the following example: * * angular.controller('MyCtrl', function($scope) { * $scope.users = [ * { id: 1, name: 'Bob' }, * { id: 2, name: 'Alice' }, * { id: 3, name: 'Steve' } * ]; * $scope.selectedUser = { id: 1, name: 'Bob' }; * }); * * *
    * * {{ user.name }} * *
    *
    * * At first one might expect that the select should be populated with "Bob" as the selected user. However, * this is not true. To determine whether something is selected, * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; * * Javascript's `==` operator does not check for deep equality (ie. that all properties * on the object are the same), but instead whether the objects are *the same object in memory*. * In this case, we have two instances of identical objects, but they exist in memory as unique * entities. Because of this, the select will have no value populated for a selected user. * * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different * expression which will be used for the equality operator. As such, we can update our `html` to * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` * element. This converts our equality expression to be * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` * which results in Bob being selected as desired. * * Working HTML: * *
    * * {{ user.name }} * *
    *
    */ function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce, $injector) { var keyCodes = $mdConstant.KEY_CODE; var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; return { restrict: 'E', require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], compile: compile, controller: function() { } // empty placeholder controller to be initialized in link }; function compile(element, attr) { // add the select value that will hold our placeholder or selected option value var valueEl = angular.element(''); valueEl.append(''); valueEl.addClass('md-select-value'); if (!valueEl[0].hasAttribute('id')) { valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); } // There's got to be an md-content inside. If there's not one, let's add it. var mdContentEl = element.find('md-content'); if (!mdContentEl.length) { element.append(angular.element('').append(element.contents())); } mdContentEl.attr('role', 'presentation'); // Add progress spinner for md-options-loading if (attr.mdOnOpen) { // Show progress indicator while loading async // Use ng-hide for `display:none` so the indicator does not interfere with the options list element .find('md-content') .prepend(angular.element( '
    ' + ' ' + '
    ' )); // Hide list [of item options] while loading async element .find('md-option') .attr('ng-show', '$$loadingAsyncDone'); } if (attr.name) { var autofillClone = angular.element(',
    That doesn't look like a valid public key.
    Too long name.
    save private key
    That doesn't look like a valid user.
    ././@LongLink0000000000000000000000000000022300000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__keypair/os__nova__keypair.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000526413737340771034020 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Nova::KeyPair angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNovaKeypairSettings', { resource_key: "OS__Nova__KeyPair", admin: false, icon: { class: 'fa-key ', name: 'OS::Nova::KeyPair', code: '\uf084', color: '#483dff' }, label: 'name', outputs: [ {'property': 'private_key', } ], modal_component: '', edge_settings: null, necessary_properties: { 'name': null }, } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNovaKeypairSettings', 'hotgenGlobals', function( osNovaKeypairSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNovaKeypairSettings.resource_key, osNovaKeypairSettings.icon); hotgenGlobals.update_resource_components( osNovaKeypairSettings.resource_key, osNovaKeypairSettings.modal_component); hotgenGlobals.update_node_labels( osNovaKeypairSettings.resource_key, osNovaKeypairSettings.label); hotgenGlobals.set_resource_outputs( osNovaKeypairSettings.resource_key, osNovaKeypairSettings.outputs); }]); function osNovaKeypairController($scope, hotgenGlobals, validationRules){ $scope.options = hotgenGlobals.get_resource_options(); $scope.admin = $scope.options.auth.admin; this.$onInit = function(){ $scope.dependson = this.dependson; } $scope.validate_name = validationRules['name']; $scope.validate_keypair = validationRules['keypair']; } osNovaKeypairController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNovaKeypairPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNovaKeypairPath(basePath){ return basePath + 'js/resources/os__nova__keypair/os__nova__keypair.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNovaKeypair', { templateUrl: osNovaKeypairPath, controller: osNovaKeypairController, bindings:{ 'keypair': '=', 'dependson': '=', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__keypair/os__nova__keypair.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000230213737340771034006 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-nova-keypair', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''+ ''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); }); })(); ././@LongLink0000000000000000000000000000022400000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__keypair/os__nova__keypair.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000611313737340771034012 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000020200000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000023300000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000703013737340771034011 0ustar zuulzuul00000000000000
    That doesn't look like a valid container name.
    Your container name is too long.
    Purge on Delete
    ././@LongLink0000000000000000000000000000024000000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000363713737340771034022 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023200000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000363713737340771034022 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023700000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000364313737340771034017 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023700000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000363513737340771034020 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023600000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000364313737340771034017 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023100000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000736613737340771034025 0ustar zuulzuul00000000000000(function() { 'use strict'; var c_meta_key = "X-Container-Meta", a_meta_key = "X-Account-Meta", c_read_key = "X-Container-Read", c_write_key = "X-Container-Write", purge_key = "PurgeOnDelete"; /** * OS::Swift::Container */ angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .value('osSwiftContainerSettings', { resource_key: "OS__Swift__Container", admin: false, icon: { class: 'fa-archive ', name: 'OS::Swift::Container', code: '\uf0a0', color: '#0bb238' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: { name: null } } ) // Register the resource to globals angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osSwiftContainerSettings','hotgenGlobals', function(osSwiftContainerSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osSwiftContainerSettings.resource_key , osSwiftContainerSettings.icon); hotgenGlobals.update_resource_components( osSwiftContainerSettings.resource_key, osSwiftContainerSettings.modal_component); hotgenGlobals.update_node_labels( osSwiftContainerSettings.resource_key, osSwiftContainerSettings.label); }]); // Define controller function osSwiftContainerController($scope, hotgenGlobals, hotgenNotify, validationRules) { this.$onInit = function(){ // Initialize X-Container-Meta if (typeof this.container[c_meta_key] === 'undefined'){ this.container[c_meta_key] = [{}]; } // Initialize X-Account-Meta if (typeof this.container[a_meta_key] === 'undefined'){ this.container[a_meta_key] = [{}]; } // Intialize Purge On Delete if (typeof this.container[purge_key] === 'undefined'){ this.container[purge_key] = false; } }; $scope.options = hotgenGlobals.get_resource_options(); $scope.show_more = false; $scope.validate_name = validationRules['name']; // Container Metadata manipulation functions this.add_x_container_meta = function(){ this.container[c_meta_key].push({}) } this.delete_x_container_meta = function(index){ this.container[c_meta_key].splice(index, 1) } // Account Metadata manipulation functions this.add_x_account_meta = function(){ this.container[a_meta_key].push({}) } this.delete_x_account_meta = function(index){ this.container[a_meta_key].splice(index, 1) } } function osSwiftContainerPath (basePath){ return basePath + 'js/resources/os__swift__container/os__swift__container.html'; } osSwiftContainerController.$inject = [ '$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osSwiftContainerPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('osSwiftContainer', { templateUrl: osSwiftContainerPath, controller: osSwiftContainerController, bindings: { 'container': '=', 'formReference': '<', } }); })(); ././@LongLink0000000000000000000000000000023600000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__swift__container/os__swift__container.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000543313737340771034016 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-swift-container', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with resource set', function() { $scope.resource = {metadata: [], scheduler_hints:[]}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); // X-Container-Meta it('x-container-meta should be successfully added', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.add_x_container_meta(); expect($scope.resource['X-Container-Meta'].length).toEqual(2); }); it('x-container-meta should be successfully deleted', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.delete_x_container_meta(); expect($scope.resource['X-Container-Meta'].length).toEqual(0); }); // X-Account-Meta it('x-account-meta should be successfully added', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.add_x_account_meta(); expect($scope.resource['X-Account-Meta'].length).toEqual(2); }); it('x-account-meta should be successfully deleted', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.delete_x_account_meta(); expect($scope.resource['X-Account-Meta'].length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000020100000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000611713737340771034016 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023600000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000611713737340771034016 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000721413737340771034015 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-subnet', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_options({ networks: [{id: 'network1-id', name: 'network1-id'}], }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with connectedoptions set', function(){ $scope.resource = {network: 'network-id',} $scope.connectedoptions = {network: [{value: 'network-id'}]}; element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Basic"); }); it('allocation_pool should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_allocation_pool(); expect($scope.resource.allocation_pools.length).toEqual(2); }); it('allocation_pool should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_allocation_pool(); expect($scope.resource.allocation_pools.length).toEqual(0); }); it('hostroute should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_hostroute(); expect($scope.resource.host_routes.length).toEqual(2); }); it('hostroute should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_hostroute(); expect($scope.resource.host_routes.length).toEqual(0); }); it('validate dns', function() { expect( $isolateScope.validate_dns('@')).toEqual(null); expect( $isolateScope.validate_dns('8.8.8.8')).toEqual('8.8.8.8'); }); }); })(); ././@LongLink0000000000000000000000000000023500000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000612313737340771034013 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001263713737340771034022 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::Subnet angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronSubnetSettings', { resource_key: "OS__Neutron__Subnet", admin: false, icon: { class: 'fa-cloud-upload ', name: 'OS::Neutron::Subnet', code: '\uf0ee', color: '#40a5f2' }, label: 'name', modal_component: '', edge_settings: { 'OS__Neutron__Net': { 'type': 'property', 'property': 'network', 'limit': 1, }, }, necessary_properties: { 'network': ['OS__Neutron__Net'] } } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronSubnetSettings', 'hotgenGlobals', function(osNeutronSubnetSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronSubnetSettings.resource_key, osNeutronSubnetSettings.icon); hotgenGlobals.update_resource_components( osNeutronSubnetSettings.resource_key, osNeutronSubnetSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronSubnetSettings.resource_key, osNeutronSubnetSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronSubnetSettings.resource_key, osNeutronSubnetSettings.label); }]); function osNeutronSubnetController($scope, hotgenGlobals, hotgenNotify, validationRules) { this.$onInit = function(){ this.admin = $scope.options.auth.admin; if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } this.disable = {'network': false} $scope.networks = $scope.get_networks_options(); if (typeof this.subnet.allocation_pools === 'undefined'){ this.subnet.allocation_pools = [{}]; } if (typeof this.subnet.host_routes === 'undefined'){ this.subnet.host_routes = [{}]; } if (typeof this.subnet.dns_nameservers === 'undefined'){ this.subnet.dns_nameservers = []; } if ( $scope.connected_options.network && $scope.connected_options.network.length > 0){ this.subnet['network'] = $scope.connected_options.network[0].value this.disable.network = true } $scope.dependson = this.dependson; } this.add_allocation_pool = function(){ this.subnet.allocation_pools.push({}) } this.delete_allocation_pool = function(index){ this.subnet.allocation_pools.splice(index, 1) } this.add_hostroute = function(){ this.subnet.host_routes.push({}) } this.delete_hostroute = function(index){ this.subnet.host_routes.splice(index, 1) } $scope.validate_dns = function (input_string){ var re = /^[A-Za-z0-9_.-]+$/; var match = re.exec(input_string); if (match){ return input_string; } else{ hotgenNotify.show_error('Invalid name server address.'); return null; } } $scope.options = hotgenGlobals.get_resource_options(); $scope.validate_name = validationRules['name']; $scope.validate_ip_address = validationRules['ip_address']; $scope.validate_cidr = validationRules['cidr']; $scope.validate_uuid4 = validationRules['uuid4']; $scope.validate_uuid_nohyphen = validationRules['uuid_nohyphen']; $scope.get_networks_options = function(){ if ('network' in $scope.connected_options){ var resource_nws = []; for (var idx in $scope.connected_options.network){ var item = $scope.connected_options.network[idx]; resource_nws.push({ id: item.value, name: item.value }) } return $scope.options.networks.concat(resource_nws); } return $scope.options.networks; } } osNeutronSubnetController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNeutronSubnetPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronSubnetPath(basePath){ return basePath + 'js/resources/os__neutron__subnet/os__neutron__subnet.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronSubnet', { templateUrl: osNeutronSubnetPath, controller: osNeutronSubnetController, bindings:{ 'subnet': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000023500000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000612413737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000612313737340771034013 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023100000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__subnet/os__neutron__subnet.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002202613737340771034013 0ustar zuulzuul00000000000000
    That doesn't look like a valid network name.
    Too long network name.
    {$ nw.name $}
    You must supply a network.
    4 6
    Please input a valid cidr range.
    Enable DHCP
    {$ 'Show More Properties' | translate $}
    Please input a valid address.
    That doesn't look like a valid subnet pool id.
    That doesn't look like a valid ip address.
    That doesn't look like a valid ip address.
    That doesn't look like a valid ip address.
    That doesn't look like a valid ip address.
    That doesn't look like a valid tenant id.
    ././@LongLink0000000000000000000000000000021200000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000025600000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000567513737340771034026 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025100000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001411513737340771034013 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::RouterInterface angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronRouterInterfaceSettings', { resource_key: "OS__Neutron__RouterInterface", admin: false, icon: { class: 'fa-sun-o', name: 'OS::Neutron::RouterInterface', code: '\uf185', color: '#40a5f2' }, modal_component: '', edge_settings: { 'OS__Neutron__Port': { 'type': 'property', 'property': 'port', 'limit': 1, }, 'OS__Neutron__Router': { 'type': 'property', 'property': 'router', 'limit': 1, }, 'OS__Neutron__Subnet': { 'type': 'property', 'property': 'subnet', 'limit': 1, }, }, necessary_properties: { 'router': ['OS__Neutron__Router'], } } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronRouterInterfaceSettings', 'hotgenGlobals',function( osNeutronRouterInterfaceSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronRouterInterfaceSettings.resource_key, osNeutronRouterInterfaceSettings.icon); hotgenGlobals.update_resource_components( osNeutronRouterInterfaceSettings.resource_key, osNeutronRouterInterfaceSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronRouterInterfaceSettings.resource_key, osNeutronRouterInterfaceSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronRouterInterfaceSettings.resource_key, osNeutronRouterInterfaceSettings.label); }]); function osNeutronRouterInterfaceController($scope, hotgenGlobals) { this.$onInit = function(){ if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } this.disable = {'port': false, 'router': false, 'subnet': false} $scope.update = { subnets: $scope.get_subnets_options(), routers: $scope.get_routers_options(), ports: $scope.get_ports_options(), } if ( $scope.connected_options.port && $scope.connected_options.port.length > 0){ this.routerinterface['port'] = $scope.connected_options.port[0].value this.disable.port = true } if ( $scope.connected_options.router && $scope.connected_options.router.length > 0){ this.routerinterface['router'] = $scope.connected_options.router[0].value this.disable.router = true } if ( $scope.connected_options.subnet && $scope.connected_options.subnet.length > 0){ this.routerinterface['subnet'] = $scope.connected_options.subnet[0].value this.disable.subnet = true } $scope.dependson = this.dependson; } $scope.options = hotgenGlobals.get_resource_options(); $scope.get_subnets_options = function(){ if ('subnet' in $scope.connected_options){ var resource_subnets = []; for (var idx in $scope.connected_options.subnet){ var item = $scope.connected_options.subnet[idx]; resource_subnets.push({ id: item.value, name: item.value }) } return $scope.options.subnets.concat(resource_subnets); } return $scope.options.subnets; } $scope.get_ports_options = function(){ if ('port' in $scope.connected_options){ var resource_ports = []; for (var idx in $scope.connected_options.port){ var item = $scope.connected_options.port[idx]; resource_ports.push({ id: item.value, name: item.value }) } return $scope.options.ports.concat(resource_ports); } return $scope.options.ports; } $scope.get_routers_options = function(){ if ('router' in $scope.connected_options){ var resource_routers = []; for (var idx in $scope.connected_options.router){ var item = $scope.connected_options.router[idx]; resource_routers.push({ id: item.value, name: item.value }) } return $scope.options.routers.concat(resource_routers); } return $scope.options.routers; } } osNeutronRouterInterfaceController.$inject = ['$scope', 'hotgenGlobals', ]; osNeutronRouterInterfacePath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronRouterInterfacePath(basePath){ return basePath + 'js/resources/os__neutron__routerinterface/os__neutron__routerinterface.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronRouterInterface', { templateUrl: osNeutronRouterInterfacePath, controller: osNeutronRouterInterfaceController, bindings:{ 'routerinterface': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000025200000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000567613737340771034027 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025700000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000567513737340771034026 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025700000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000567513737340771034026 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025600000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000613313737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-routerinterface', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_options({ routers: [{id: 'router1-id', name: 'router1-id'}], subnets: [{id: 'subnet1-id', name: 'subnet1-id'}], ports: [{id: 'port1-id', name: 'port1-id'}], }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''+ ''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with connectedoptions set', function(){ $scope.resource = {port: 'port-id', subnet: 'subnet-id', router: 'router-id'} $scope.connectedoptions = {port: [{value: 'port-id'}], subnet: [{value: 'subnet-id'}], router: [{value: 'router-id'}], }; element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); }); })(); ././@LongLink0000000000000000000000000000026000000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000566313737340771034023 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025300000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__routerinterface/os__neutron__routerinterface.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000360513737340771034015 0ustar zuulzuul00000000000000

    {$ $ctrl.message $}

    {$ router.name $}
    You must supply a router.
    {$ port.name $} {$ subnet.name $}
    ././@LongLink0000000000000000000000000000021000000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000024700000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002055213737340771034015 0ustar zuulzuul00000000000000
    You did not enter a valid integer.
    You did not enter a non-negative number.
    You must supply a maximum size.
    You must supply an integer.
    You did not enter a valid integer.
    You did not enter a non-negative number.
    You must supply a minimum size.
    You must supply an integer.
    existing items uploading a template file
    You did not specify Resource Type.
    {$ resource_type $}
    choose a file
    You did not enter a valid integer.
    You did not enter a non-negative number.
    You must supply an integer.
    You did not enter a valid integer.
    You did not enter a non-negative number.
    You must supply an integer.
    You did not enter a valid integer.
    You did not enter a non-negative number.
    You must supply an integer.
    You did not enter a valid number.
    You did not enter a non-negative number.
    You did not enter a valid integer.
    You did not enter a positive number.
    You must supply an integer.
    ././@LongLink0000000000000000000000000000025300000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000470513737340771034017 0ustar zuulzuul00000000000000 Layer 1Layer 2 ././@LongLink0000000000000000000000000000025200000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000470513737340771034017 0ustar zuulzuul00000000000000 Layer 1Layer 2 ././@LongLink0000000000000000000000000000025200000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001104213737340771034007 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-heat-auto-scaling-group', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenNotify; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenNotify = $injector.get('hotgenNotify'); spyOn(hotgenNotify, 'show_success'); spyOn(hotgenNotify, 'show_error'); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); expect($isolateScope.is_upload).toEqual('false'); }); it('find tab title with resource set', function() { $scope.resource = {resource: {type: 'filepath.yaml'}}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(' Layer 1Layer 2 ././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000470313737340771034015 0ustar zuulzuul00000000000000 Layer 1Layer 2 ././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001302713737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; /* OS::Heat::AutoScalingGroup * */ angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .value('osHeatAutoScalingGroupSettings', { resource_key: "OS__Heat__AutoScalingGroup", admin: false, icon: { class: 'fa-hdd-o', name: 'OS::Heat::AutoScalingGroup', code: '\uf0a0', color: '#0bb238' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: ['max_size', 'min_size', 'resource'] } ) // Register the resource to globals angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osHeatAutoScalingGroupSettings','hotgenGlobals', function(osHeatAutoScalingGroupSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osHeatAutoScalingGroupSettings.resource_key , osHeatAutoScalingGroupSettings.icon); hotgenGlobals.update_resource_components( osHeatAutoScalingGroupSettings.resource_key, osHeatAutoScalingGroupSettings.modal_component); hotgenGlobals.update_node_labels( osHeatAutoScalingGroupSettings.resource_key, osHeatAutoScalingGroupSettings.label); }]); // Define controller function osHeatAutoScalingGroupController($scope, hotgenGlobals, hotgenNotify, validationRules) { this.$onInit = function(){ $scope.dependson = this.dependson; if (typeof this.autoscalinggroup.resource === 'undefined'){ this.autoscalinggroup.resource = {'type': '', 'properties': [{}]}; } if (typeof this.autoscalinggroup.rolling_updates === 'undefined'){ this.autoscalinggroup.rolling_updates = {}; } if (this.autoscalinggroup.resource.type){ $scope.filecontent = hotgenGlobals.get_reference_file(this.autoscalinggroup.resource.type); } }; $scope.validate_integer = validationRules['integer']; $scope.controller = this; $scope.resource_types = []; var resource_types = hotgenGlobals.get_resource_types(); resource_types.forEach(function(element){ $scope.resource_types.push(element.replace(/_/g, ':')); }); $scope.options = hotgenGlobals.get_resource_options(); $scope.update_upload = function(){ if ($scope.controller.autoscalinggroup && $scope.controller.autoscalinggroup.resource && $scope.controller.autoscalinggroup.resource.type.indexOf('.yaml') != -1){ return 'true'; } return 'false'; } $scope.is_upload = $scope.update_upload(); $scope.$watch('is_upload', function(newValue, oldValue){ if (oldValue === newValue){ return; } if (newValue === true || newValue === 'true'){ $scope.controller.autoscalinggroup.resource.properties = [{}]; if (!($scope.filecontent && $scope.filecontent.length >= 0)){ $scope.controller.autoscalinggroup.resource.type = ''; } } else{ // ; } }); $scope.file_upload = function(element){ var file = element.files[0]; if (!file){ return; } var textType = /\.yaml$/; if (file.name.match(textType)) { var reader = new FileReader(); $scope.controller.autoscalinggroup.resource.type = file.name; reader.onload = function(e) { hotgenNotify.show_success('Read file content.'); hotgenGlobals.set_reference_file(file.name, reader.result) $scope.filecontent = reader.result; } reader.readAsText(file); } else { hotgenNotify.show_error('File type is not supported.'); } } $scope.clickUpload = function(){ setTimeout(function () { angular.element('#resourcetype-file').trigger('click'); }, 0); }; this.delete_property = function(index){ this.autoscalinggroup.resource.properties.splice(index, 1); } this.add_property = function(){ this.autoscalinggroup.resource.properties.push({}); } } function osHeatAutoScalingGroupPath (basePath){ return basePath + 'js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup.html'; } osHeatAutoScalingGroupController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osHeatAutoScalingGroupPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('osHeatAutoscalinggroup', { templateUrl: osHeatAutoScalingGroupPath, controller: osHeatAutoScalingGroupController, bindings: { 'autoscalinggroup': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(); ././@LongLink0000000000000000000000000000025300000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__autoscalinggroup/os__heat__autoscalinggroup-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000460013737340771034011 0ustar zuulzuul00000000000000Layer 1Layer 2 ././@LongLink0000000000000000000000000000017700000000000011222 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000430213737340771034010 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022500000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002411113737340771034010 0ustar zuulzuul00000000000000
    That doesn't look like a valid port name.
    Too long port name.
    {$ nw.name $}
    You must supply a network.
    {$ sg.name $}
    Admin State UP Port Security Enabled
    {$ 'Show More Properties' | translate $}
    That doesn't look like a valid mac address.
    That doesn't look like a valid ip address.

    That doesn't look like a valid ip address.
    {$ subnet.name $}
    {$ item.display $} Warning: "{$ $ctrl.searchText $}" saved with no pre-defined device owner found. {$ qos.name $}
    That doesn't look like a valid mac address.
    ././@LongLink0000000000000000000000000000023100000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000430213737340771034010 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022300000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002313413737340771034014 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::Port angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronPortSettings', { resource_key: "OS__Neutron__Port", admin: false, icon: { class: 'fa-genderless', name: 'OS::Neutron::Port', code: '\uf22d', color: '#40a5f2', }, label: 'name', modal_component: '', edge_settings: { 'OS__Neutron__Net': { 'type': 'property', 'property': 'network', 'limit': 1, }, 'OS__Neutron__Subnet': { 'type': 'complex', 'property': 'fixed_ips.subnet', 'limit': 99, }, 'OS__Neutron__SecurityGroup': { 'type': 'list', 'property': 'security_groups', 'limit': 99, }, }, necessary_properties: { 'network': ['OS__Neutron__Net'], } } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronPortSettings', 'hotgenGlobals', function(osNeutronPortSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronPortSettings.resource_key, osNeutronPortSettings.icon); hotgenGlobals.update_resource_components( osNeutronPortSettings.resource_key, osNeutronPortSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronPortSettings.resource_key, osNeutronPortSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronPortSettings.resource_key, osNeutronPortSettings.label); }]); function osNeutronPortController($scope, hotgenGlobals, validationRules){ this.$onInit = function(){ if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } this.disable = { 'network': false, 'subnets': [], 'security_groups': [], } if (typeof this.port.admin_state_up === 'undefined'){ this.port.admin_state_up = true; } if (typeof this.port.binding === 'undefined'){ this.port.binding = {'vnic_type': ''}; } if (typeof this.port.allowed_address_pairs === 'undefined'){ this.port.allowed_address_pairs = [{}]; } if (typeof this.port.fixed_ips === 'undefined'){ this.port.fixed_ips = [{}]; } if (typeof this.port.security_groups === 'undefined'){ this.port.security_groups = []; } if (typeof this.port.value_specs == 'undefined'){ this.port.value_specs = [{}] } if (this.port.device_owner){ this.searchText = this.port.device_owner; } if ( $scope.connected_options.network && $scope.connected_options.network.length > 0){ this.port['network'] = $scope.connected_options.network[0].value; this.disable.network = true; } if ( $scope.connected_options.security_groups && $scope.connected_options.security_groups.length > 0){ for (var idx in $scope.connected_options.security_groups){ this.port['security_groups'].push($scope.connected_options.security_groups[idx].value); this.disable.security_groups.push($scope.connected_options.security_groups[idx].value); } } if ( $scope.connected_options['fixed_ips.subnet'] && $scope.connected_options['fixed_ips.subnet'].length > 0){ for (var idx in $scope.connected_options['fixed_ips.subnet']){ this.port.fixed_ips.splice(0,0, {subnet: $scope.connected_options['fixed_ips.subnet'][idx].value}); this.disable.subnets.push($scope.connected_options['fixed_ips.subnet'][idx].value); } } $scope.update = { networks: $scope.get_networks_options(), security_groups: $scope.get_security_groups_options(), subnets: $scope.get_subnets_options(), } $scope.dependson = this.dependson; }; $scope.options = hotgenGlobals.get_resource_options(); $scope.show_more = false; $scope.validate_name = validationRules['name']; $scope.validate_mac_address = validationRules['mac_address']; $scope.validate_ip_address = validationRules['ip_address']; this.device_owners = load_device_owners(); this.querySearch = querySearch; this.show_not_found = true; this.selectedItemChange = selectedItemChange; this.searchTextChange = searchTextChange; $scope.get_networks_options = function(){ if ('network' in $scope.connected_options){ var resource_nw = []; for (var idx in $scope.connected_options.network){ var item = $scope.connected_options.network[idx]; resource_nw.push({ id: item.value, name: item.value }) } return $scope.options.networks.concat(resource_nw); } return $scope.options.networks; } $scope.get_security_groups_options = function(){ if ('security_groups' in $scope.connected_options){ var resource_secgroups = []; for (var idx in $scope.connected_options.security_groups){ var item = $scope.connected_options.security_groups[idx]; resource_secgroups.push({ id: item.value, name: item.value }) } return $scope.options.security_groups.concat(resource_secgroups); } return $scope.options.security_groups; } $scope.get_subnets_options = function(){ if ('fixed_ips.subnet' in $scope.connected_options){ var resource_subnets = []; for (var idx in $scope.connected_options['fixed_ips.subnet']){ var item = $scope.connected_options['fixed_ips.subnet'][idx]; resource_subnets.push({ id: item.value, name: item.value }) } return $scope.options.subnets.concat(resource_subnets); } return $scope.options.subnets; } $scope.qos_policies = $scope.options.qos_policies; this.add_value_specs = function(){ this.port.value_specs.push({}) } this.delete_value_specs = function(index){ this.port.value_specs.splice(index, 1) } this.add_allowed_address_pair = function(){ this.port.allowed_address_pairs.push({}) } this.add_fixed_ip = function(){ this.port.fixed_ips.push({}) } this.delete_allowed_address_pair = function(index){ this.port.allowed_address_pairs.splice(index, 1) } this.delete_fixed_ip = function(index){ this.port.fixed_ips.splice(index, 1) } function searchTextChange(text) { this.port.device_owner = text; } function selectedItemChange(item) { this.port.device_owner = item.display; } function load_device_owners(){ var all_device_owners = 'network:floatingip, network:router_interface, network:dhcp'; return all_device_owners.split(/, +/g).map(function(dev_owner){ return { value: dev_owner.replace(/:/g, '_'), display: dev_owner } }); } function querySearch (query) { return query ? this.device_owners.filter( createFilterFor(query) ) : this.device_owners; } /** * Create filter function for a query string */ function createFilterFor(query) { var lowercaseQuery = angular.lowercase(query).replace(/:/g, '_'); return function filterFn(dev_owner) { return (dev_owner.value.indexOf(lowercaseQuery) === 0); }; } } osNeutronPortController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNeutronPortPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronPortPath(basePath){ return basePath + 'js/resources/os__neutron__port/os__neutron__port.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronPort', { templateUrl: osNeutronPortPath, controller: osNeutronPortController, bindings:{ 'port': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000023200000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000427613737340771034022 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001246013737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-port', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_options({ networks: [{id: 'network1-id', name: 'network1-id'}], security_groups: [{id: 'secgroup1-id', name: 'secgroup1-id'}], subnets: [{id: 'subnet1-id', name: 'subnet-id'}] }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with connectedoptions set', function() { $scope.resource = {device_owner: 'someone', network: 'network-id', security_groups: ['secgroup-id'], 'fixed_ips.subnet': ['subnet-id'],} $scope.connectedoptions = {network: [{value: 'network-id'}], security_groups: [{value: 'secgroup-id'}], 'fixed_ips.subnet': [{value: 'subnet-id'}], }; element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('value_spec should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_value_specs(); expect($scope.resource.value_specs.length).toEqual(2); }); it('value_spec should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_value_specs(); expect($scope.resource.value_specs.length).toEqual(0); }); it('allowed address pair should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_allowed_address_pair(); expect($scope.resource.allowed_address_pairs.length).toEqual(2); }); it('allowed address pair should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_allowed_address_pair(); expect($scope.resource.allowed_address_pairs.length).toEqual(0); }); it('fixed_ip should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_fixed_ip(); expect($scope.resource.fixed_ips.length).toEqual(2); }); it('fixed_ip should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_fixed_ip(); expect($scope.resource.fixed_ips.length).toEqual(0); }); it('searchTextChange set device_owner', function(){ var $ctrl = $isolateScope.$ctrl; $ctrl.searchTextChange('device_owner_01'); expect($scope.resource.device_owner).toEqual('device_owner_01'); }); it('selectedItemChange set device_owner', function(){ var $ctrl = $isolateScope.$ctrl; $ctrl.selectedItemChange({display: 'device_owner_01'}); expect($scope.resource.device_owner).toEqual('device_owner_01'); }); it('querySearch set device_owner', function(){ var $ctrl = $isolateScope.$ctrl; var retValue = $ctrl.querySearch(''); expect(retValue.length).toEqual(3); retValue = $ctrl.querySearch('network:dhcp'); expect(retValue).toEqual([ { value: 'network_dhcp', display: 'network:dhcp' }]); }); }); })(); ././@LongLink0000000000000000000000000000023100000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000430213737340771034010 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022400000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__port/os__neutron__port.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000427613737340771034022 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000020500000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002416513737340771034021 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024000000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002412313737340771034013 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002416513737340771034021 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002416513737340771034021 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001204013737340771034006 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-heat-resource-group', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenNotify; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenNotify = $injector.get('hotgenNotify'); spyOn(hotgenNotify, 'show_success'); spyOn(hotgenNotify, 'show_error'); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); expect($isolateScope.is_upload).toEqual('false'); }); it('find tab title with resource set', function() { $scope.resource = {resource_def: {type: 'filepath.yaml'}}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); expect($isolateScope.is_upload ).toEqual('true'); }); it('$scope.is_upload should be successfully watched', function() { $isolateScope.is_upload = 'true'; $isolateScope.$digest(); $isolateScope.is_upload = 'false'; $isolateScope.$digest(); $isolateScope.filecontent = 'some thing here'; $isolateScope.is_upload = 'true'; $isolateScope.$digest(); expect($isolateScope.controller.resourcegroup.resource_def.type ).toEqual(''); }); it('file should be successfully uploaded', function() { var blob = new Blob([''], {type: '', }); blob['name'] = 'filename.yaml' var upload_element = {files: [blob], } spyOn(window, 'FileReader').and.returnValue({ readAsText: function(file) { this.onload({}); }, result: 'file contents.' }); $isolateScope.file_upload(upload_element); expect($isolateScope.filecontent).toEqual('file contents.'); expect(hotgenNotify.show_success).toHaveBeenCalled(); }); it('file should be not uploaded return undefined', function() { var upload_element = {files: []} var returnValue = $isolateScope.file_upload(upload_element); expect(returnValue).toEqual(undefined); }); it('file should be not uploaded show error', function() { var upload_element = {files: [{name: 'file.txt'}]} var returnValue = $isolateScope.file_upload(upload_element); expect(hotgenNotify.show_error).toHaveBeenCalled(); }); it('metadata should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_metadata(); expect($ctrl.resourcegroup.resource_def.metadata.length).toEqual(1); }); it('metadata should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_metadata(); $ctrl.delete_metadata(); expect($ctrl.resourcegroup.resource_def.metadata.length).toEqual(0); }); it('property should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_property(); expect($ctrl.resourcegroup.resource_def.properties.length).toEqual(2); }); it('property should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_property(); expect($ctrl.resourcegroup.resource_def.properties.length).toEqual(0); }); it('click upload', function() { spyOn(window, 'setTimeout').and.callFake(function(){}); $isolateScope.clickUpload(); expect(setTimeout).toHaveBeenCalled(); }); }); })(); ././@LongLink0000000000000000000000000000024100000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001350513737340771034015 0ustar zuulzuul00000000000000
    You did not enter a valid number.
    You did not enter a positive number.
    existing items uploading a template file
    You did not select Resource Reference Type.
    {$ resource_type $}
    choose a file
    {$ 'Show More Properties' | translate $}
    ././@LongLink0000000000000000000000000000023700000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001261413737340771034015 0ustar zuulzuul00000000000000(function() { 'use strict'; /* OS::Cinder::Volume * */ angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .value('osHeatResourceGroupSettings', { resource_key: "OS__Heat__ResourceGroup", admin: false, icon: { class: 'fa-server', name: 'OS::Heat::ResourceGroup', code: '\uf233', color: '#0bb238' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: null } ); // Register the resource to globals angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osHeatResourceGroupSettings','hotgenGlobals', function(osHeatResourceGroupSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osHeatResourceGroupSettings.resource_key , osHeatResourceGroupSettings.icon); hotgenGlobals.update_resource_components( osHeatResourceGroupSettings.resource_key, osHeatResourceGroupSettings.modal_component); hotgenGlobals.update_node_labels( osHeatResourceGroupSettings.resource_key, osHeatResourceGroupSettings.label); }]); function osHeatResourceGroupController ($scope, hotgenGlobals, hotgenNotify, validationRules) { this.$onInit = function(){ if (typeof this.resourcegroup.resource_def === 'undefined'){ this.resourcegroup.resource_def = {'type': '', 'properties': [{}], 'metadata': []}; } $scope.dependson = this.dependson; if (this.resourcegroup.resource_def.type){ $scope.filecontent = hotgenGlobals.get_reference_file(this.resourcegroup.resource_def.type); } }; $scope.controller = this; $scope.resource_types = []; var resource_types = hotgenGlobals.get_resource_types(); resource_types.forEach(function(element){ $scope.resource_types.push(element.replace(/_/g, ':')); }); $scope.update_upload = function(){ if ($scope.controller.resourcegroup && $scope.controller.resourcegroup.resource_def && $scope.controller.resourcegroup.resource_def.type.indexOf('.yaml') != -1){ return 'true'; } return 'false'; } $scope.is_upload = $scope.update_upload(); $scope.show_more = false; $scope.$watch('is_upload', function(newValue, oldValue){ if (oldValue === newValue){ return; } if (newValue === true || newValue === 'true'){ $scope.controller.resourcegroup.resource_def.properties = [{}]; if (!($scope.filecontent && $scope.filecontent.length >= 0)){ $scope.controller.resourcegroup.resource_def.type = ''; } } else{ // ; } }); $scope.file_upload = function(element){ var file = element.files[0]; if (!file){ return; } var textType = /\.yaml$/; if (file.name.match(textType)) { var reader = new FileReader(); $scope.controller.resourcegroup.resource_def.type = file.name; reader.onload = function(e) { hotgenNotify.show_success('Read file content.'); hotgenGlobals.set_reference_file(file.name, reader.result) $scope.filecontent = reader.result; } reader.readAsText(file); } else { hotgenNotify.show_error('File type is not supported.'); } } $scope.clickUpload = function(){ setTimeout(function () { angular.element('#resourceref-file').trigger('click'); }, 0); }; this.delete_property = function(index){ this.resourcegroup.resource_def.properties.splice(index, 1); } this.add_property = function(){ this.resourcegroup.resource_def.properties.push({}); } this.delete_metadata = function(index){ this.resourcegroup.resource_def.metadata.splice(index, 1); } this.add_metadata = function(){ this.resourcegroup.resource_def.metadata.push({}); } } function osHeatResourceGroupPath (basePath){ return basePath + 'js/resources/os__heat__resourcegroup/os__heat__resourcegroup.html'; } osHeatResourceGroupController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osHeatResourceGroupPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('osHeatResourceGroup', { templateUrl: osHeatResourceGroupPath, controller: osHeatResourceGroupController, bindings: { 'resourcegroup': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(); ././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__resourcegroup/os__heat__resourcegroup-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000002414313737340771034015 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000336713737340771034022 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-designate-recordset', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title Properties with resource properties set', function() { $scope.resource = { masters: [], attributes: []}; element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); expect(element.find('span').html()).toContain("Properties"); }); }); })(); ././@LongLink0000000000000000000000000000024200000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000211513737340771034010 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSRECORD././@LongLink0000000000000000000000000000024700000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000212613737340771034012 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSRECORD././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000212613737340771034012 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSRECORD././@LongLink0000000000000000000000000000025000000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000211313737340771034006 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSRECORD././@LongLink0000000000000000000000000000024700000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000212613737340771034012 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSRECORD././@LongLink0000000000000000000000000000024100000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001015313737340771034011 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Designate::RecordSet angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osDesignateRecordsetSettings', { resource_key: "OS__Designate__RecordSet", admin: false, icon: { class: 'fa-key ', name: 'OS::Designate::RecordSet', code: '\uf084', color: '#483dff' }, label: 'name', modal_component: '', edge_settings: { 'OS__Designate__Zone': { 'type': 'property', 'property': 'zone', 'limit': 1, }, }, necessary_properties: { 'name': null, 'zone': ['OS__Designate__Zone'], 'type': null, 'records': null, }, } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osDesignateRecordsetSettings', 'hotgenGlobals', function( osDesignateRecordsetSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osDesignateRecordsetSettings.resource_key, osDesignateRecordsetSettings.icon); hotgenGlobals.update_resource_components( osDesignateRecordsetSettings.resource_key, osDesignateRecordsetSettings.modal_component); hotgenGlobals.update_edge_directions( osDesignateRecordsetSettings.resource_key, osDesignateRecordsetSettings.edge_settings); hotgenGlobals.update_node_labels( osDesignateRecordsetSettings.resource_key, osDesignateRecordsetSettings.label); }]); function osDesignateRecordsetController($scope, hotgenGlobals, validationRules){ $scope.options = hotgenGlobals.get_resource_options(); $scope.options.types = [ {'id': 'A', 'name': 'A'}, {'id': 'AAAA', 'name': 'AAAA'}, {'id': 'MX', 'name': 'MX'}, {'id': 'CNAME', 'name': 'CNAME'}, {'id': 'TXT', 'name': 'TXT'}, {'id': 'SRV', 'name': 'SRV'}, {'id': 'NS', 'name': 'NS'}, {'id': 'PTR', 'name': 'PTR'}, {'id': 'SPF', 'name': 'SPF'}, {'id': 'SSHFP', 'name': 'SSHFP'}, ]; $scope.admin = $scope.options.auth.admin; this.$onInit = function(){ if (typeof this.recordset.records === 'undefined'){ this.recordset.records = []; } if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } this.disable = {'zone': false} if ( $scope.connected_options.zone && $scope.connected_options.zone.length > 0){ this.recordset.zone = $scope.connected_options.zone[0].value this.disable.zone = true } $scope.dependson = this.dependson; } $scope.validate_name = validationRules['name']; } osDesignateRecordsetController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osDesignateRecordsetPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osDesignateRecordsetPath(basePath){ return basePath + 'js/resources/os__designate__recordset/os__designate__recordset.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osDesignateRecordset', { templateUrl: osDesignateRecordsetPath, controller: osDesignateRecordsetController, bindings:{ 'recordset': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000024300000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__recordset/os__designate__recordset.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000667713737340771034031 0ustar zuulzuul00000000000000
    You must supply a name.
    That doesn't look like a valid name.
    Too long name.
    You must supply a zone.
    That doesn't look like a valid id of zone.
    {$ type.name $}
    At least one record is required.
    {$ 'Show More Properties' | translate $}
    Too long description.
    ././@LongLink0000000000000000000000000000020100000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000175213737340771034016 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSZONE././@LongLink0000000000000000000000000000023600000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000175013737340771034014 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSZONE././@LongLink0000000000000000000000000000023500000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000176313737340771034020 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSZONE././@LongLink0000000000000000000000000000023100000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000627713737340771034025 0ustar zuulzuul00000000000000
    You must supply a name.
    That doesn't look like a valid name.
    Too long name.
    You must supply an email.
    Too long description.
    {$ 'Show More Properties' | translate $}
    {$ type.name $}
    ././@LongLink0000000000000000000000000000023500000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000175713737340771034023 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSZONE././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000176313737340771034020 0ustar zuulzuul00000000000000 Layer 1 Layer 1DNSZONE././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000371613737340771034020 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-designate-zone', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''+ ''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title Properties with resource properties set', function() { $scope.resource = { masters: [], attributes: []}; element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); expect(element.find('span').html()).toContain("Properties"); }); it('test validate master function', function() { expect($isolateScope.validate_master('192.168.1.1')).toEqual('192.168.1.1'); expect($isolateScope.validate_master('localhost')).toEqual('localhost'); expect($isolateScope.validate_master('xxx@gmail.com')).toEqual(null); }); }); })(); ././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__designate__zone/os__designate__zone.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000634413737340771034020 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Designate::Zone angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osDesignateZoneSettings', { resource_key: "OS__Designate__Zone", admin: false, icon: { class: 'fa-key ', name: 'OS::Designate::Zone', code: '\uf084', color: '#483dff' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: { 'name': null, 'email': null }, } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osDesignateZoneSettings', 'hotgenGlobals', function( osDesignateZoneSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osDesignateZoneSettings.resource_key, osDesignateZoneSettings.icon); hotgenGlobals.update_resource_components( osDesignateZoneSettings.resource_key, osDesignateZoneSettings.modal_component); hotgenGlobals.update_edge_directions( osDesignateZoneSettings.resource_key, osDesignateZoneSettings.edge_settings); hotgenGlobals.update_node_labels( osDesignateZoneSettings.resource_key, osDesignateZoneSettings.label); }]); function osDesignateZoneController($scope, hotgenGlobals, hotgenNotify, validationRules){ $scope.options = hotgenGlobals.get_resource_options(); $scope.options.types = [ {'id': 'PRIMARY', 'name': 'PRIMARY'}, {'id': 'SECONDARY', 'name': 'SECONDARY'} ]; $scope.admin = $scope.options.auth.admin; this.$onInit = function(){ if (typeof this.zone.masters === 'undefined'){ this.zone.masters = []; } $scope.dependson = this.dependson; } $scope.validate_zone = validationRules['zone']; $scope.validate_master = function (input_string){ var re = /^[A-Za-z0-9_.-]+$/; var match = re.exec(input_string); if (match){ return input_string; } else{ hotgenNotify.show_error('Invalid master value.'); return null; } } } osDesignateZoneController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osDesignateZonePath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osDesignateZonePath(basePath){ return basePath + 'js/resources/os__designate__zone/os__designate__zone.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osDesignateZone', { templateUrl: osDesignateZonePath, controller: osDesignateZoneController, bindings:{ 'zone': '=', 'dependson': '=', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000017600000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000300613737340771034010 0ustar zuulzuul00000000000000 Layer 1././@LongLink0000000000000000000000000000022100000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000657313737340771034024 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::Net angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronNetSettings', { resource_key: "OS__Neutron__Net", admin: false, icon: { class: 'fa-cloud', name: 'OS::Neutron::Net', code: '\uf0c2', color: '#40a5f2' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: null } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronNetSettings', 'hotgenGlobals', function(osNeutronNetSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronNetSettings.resource_key, osNeutronNetSettings.icon); hotgenGlobals.update_resource_components( osNeutronNetSettings.resource_key, osNeutronNetSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronNetSettings.resource_key, osNeutronNetSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronNetSettings.resource_key, osNeutronNetSettings.label); }]); function osNeutronNetController($scope, hotgenGlobals, validationRules){ this.$onInit = function(){ this.admin = $scope.options.auth.admin; if (typeof this.network.tags == 'undefined'){ this.network.tags = [] } if (typeof this.network.dhcp_agent_ids === 'undefined'){ this.network.dhcp_agent_ids = []; } if (typeof this.network.admin_state_up === 'undefined'){ this.network.admin_state_up = true; } if (typeof this.network.value_specs == 'undefined'){ this.network.value_specs = [{}] } $scope.dependson = this.dependson; }; this.add_value_specs = function(){ this.network.value_specs.push({}) } this.delete_value_specs = function(index){ this.network.value_specs.splice(index, 1) } $scope.options = hotgenGlobals.get_resource_options(); $scope.qos_policies = $scope.options.qos_policies; $scope.show_more = false; $scope.validate_name = validationRules['name']; $scope.validate_domain = validationRules['domain']; $scope.validate_uuid_nohyphe = validationRules['uuid_nohyphen']; } osNeutronNetController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNeutronNetPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronNetPath(basePath){ return basePath + 'js/resources/os__neutron__net/os__neutron__net.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronNet', { templateUrl: osNeutronNetPath, controller: osNeutronNetController, bindings:{ 'network': '=', 'dependson': '=', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000300213737340771034004 0ustar zuulzuul00000000000000 Layer 1././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000300613737340771034010 0ustar zuulzuul00000000000000 Layer 1././@LongLink0000000000000000000000000000022300000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001257113737340771034017 0ustar zuulzuul00000000000000
    That doesn't look like a valid network name.
    Too long network name.
    Please input a valid DNS Domain.
    Admin State UP Port Security Enabled Shared
    {$ 'Show More Properties' | translate $}
    {$ qos.name $} ({$ qos.id $})
    That doesn't look like a valid tenant id.
    ././@LongLink0000000000000000000000000000022600000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000300613737340771034010 0ustar zuulzuul00000000000000 Layer 1././@LongLink0000000000000000000000000000022200000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000300213737340771034004 0ustar zuulzuul00000000000000 Layer 1././@LongLink0000000000000000000000000000022600000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__net/os__neutron__net.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000417113737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-net', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''+ ''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title Properties with resource properties set', function() { $scope.resource = { tags: [], dhcp_agent_ids: [], admin_state_up: true, value_specs: []}; element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); expect(element.find('span').html()).toContain("Properties"); }); it('value_spec should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_value_specs(); expect($scope.resource.value_specs.length).toEqual(2); }); it('value_spec should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_value_specs(); expect($scope.resource.value_specs.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000020000000000000011205 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000536413737340771034021 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022500000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000672113737340771034017 0ustar zuulzuul00000000000000(function() { 'use strict'; /* OS::Cinder::Volume * */ angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .value('osCinderVolumeSettings', { resource_key: "OS__Cinder__Volume", admin: false, icon: { class: 'fa-hdd-o', name: 'OS::Cinder::Volume', code: '\uf0a0', color: '#0bb238' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: null } ) // Register the resource to globals angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osCinderVolumeSettings','hotgenGlobals', function(osCinderVolumeSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osCinderVolumeSettings.resource_key , osCinderVolumeSettings.icon); hotgenGlobals.update_resource_components( osCinderVolumeSettings.resource_key, osCinderVolumeSettings.modal_component); hotgenGlobals.update_node_labels( osCinderVolumeSettings.resource_key, osCinderVolumeSettings.label); }]); // Define controller function osCinderVolumeController($scope, hotgenGlobals, hotgenNotify, validationRules) { this.$onInit = function(){ if (typeof this.volume.metadata === 'undefined'){ this.volume.metadata = [{}]; } if (typeof this.volume.scheduler_hints === 'undefined'){ this.volume.scheduler_hints = [{}]; } $scope.dependson = this.dependson; }; $scope.boot_sources = [ {'id': 'image', 'name': 'image'}, {'id': 'volume', 'name': 'volume'}, {'id': 'volume_snapshot', 'name': 'volume snapshot'}, {'id': 'backup', 'name': 'backup'}, ]; $scope.options = hotgenGlobals.get_resource_options(); $scope.show_more = false; $scope.validate_name = validationRules['name']; this.delete_metadata = function(index){ this.volume.metadata.splice(index, 1) } this.add_metadata = function(){ this.volume.metadata.push({}) } this.delete_scheduler_hints = function(index){ this.volume.scheduler_hints.splice(index, 1) } this.add_scheduler_hints= function(){ this.volume.scheduler_hints.push({}) } } function osCinderVolumePath (basePath){ return basePath + 'js/resources/os__cinder__volume/os__cinder__volume.html'; } osCinderVolumeController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osCinderVolumePath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('osCinderVolume', { templateUrl: osCinderVolumePath, controller: osCinderVolumeController, bindings: { 'volume': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(); ././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001711113737340771034012 0ustar zuulzuul00000000000000
    That doesn't look like a valid volume name.
    Your volume name is too long.
    You did not enter a valid number.
    You did not enter a positive number.
    {$ vtype.name $} {$ az.name $} {$ 'Show More Properties' | translate $}
    Please do not input invalid characters!
    Please input the description less than 255 characters.
    multiattach readonly {$ source.name $} {$ backup.name $} {$ image.name $} {$ snapshot.name $} {$ volume.name $}
    ././@LongLink0000000000000000000000000000023300000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000537013737340771034016 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023300000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000537013737340771034016 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023200000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000561113737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-cinder-volume', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with resource set', function() { $scope.resource = {metadata: [], scheduler_hints:[]}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('metadata should be successfully added', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.add_metadata(); expect($scope.resource.metadata.length).toEqual(2); }); it('metadata should be successfully deleted', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.delete_metadata(); expect($scope.resource.metadata.length).toEqual(0); }); it('scheduler_hints should be successfully added', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.add_scheduler_hints(); expect($scope.resource.scheduler_hints.length).toEqual(2); }); it('scheduler_hints should be successfully deleted', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.delete_scheduler_hints(); expect($scope.resource.scheduler_hints.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000023200000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000537013737340771034016 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022600000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__cinder__volume/os__cinder__volume.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000536313737340771034020 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022000000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000026600000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000704213737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000027200000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000704313737340771034015 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000027300000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000704313737340771034015 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000027400000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000703313737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000026500000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001302513737340771034012 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::FloatingIPAssociation angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronFloatingipAssociationSettings', { resource_key: "OS__Neutron__FloatingIPAssociation", admin: false, icon: { class: 'fa-paperclip', name: 'OS::Neutron::FloatingIPAssociation', code: '\uf0c6', color: '#40a5f2' }, label: 'fixed_ip_address', modal_component: '', edge_settings: { 'OS__Neutron__FloatingIP': { 'type': 'property', 'property': 'floatingip_id', 'limit': 1, }, 'OS__Neutron__Port': { 'type': 'property', 'property': 'port_id', 'limit': 1, }, }, necessary_properties: { 'floatingip_id': ['OS__Neutron__FloatingIP'], 'port_id': ['OS__Neutron__Port'], } } ) angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronFloatingipAssociationSettings', 'hotgenGlobals', function(osNeutronFloatingipAssociationSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronFloatingipAssociationSettings.resource_key, osNeutronFloatingipAssociationSettings.icon); hotgenGlobals.update_resource_components( osNeutronFloatingipAssociationSettings.resource_key, osNeutronFloatingipAssociationSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronFloatingipAssociationSettings.resource_key, osNeutronFloatingipAssociationSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronFloatingipAssociationSettings.resource_key, osNeutronFloatingipAssociationSettings.label); }]); function osNeutronFloatingipAssociationController($scope, hotgenGlobals, validationRules){ this.$onInit = function(){ if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = []; } else{ $scope.connected_options = this.connectedoptions; } this.disable = {'floatingip_id': false, 'port_id': false} $scope.dependson = this.dependson; $scope.update = { floatingips: $scope.get_floatingip_options(), ports: $scope.get_port_options(), } if ( $scope.connected_options.floatingip_id && $scope.connected_options.floatingip_id.length > 0){ this.floatingipassociation['floatingip_id'] = $scope.connected_options.floatingip_id[0].value; this.disable.floatingip_id = true; } if ( $scope.connected_options.port_id && $scope.connected_options.port_id.length > 0){ this.floatingipassociation['port_id'] = $scope.connected_options.port_id[0].value; this.disable.port_id = true; } } $scope.options = hotgenGlobals.get_resource_options(); $scope.validate_ip_address = validationRules['ip_address']; $scope.get_floatingip_options = function(){ if ('floatingip_id' in $scope.connected_options){ var resource_fip = []; for (var idx in $scope.connected_options.floatingip_id){ var item = $scope.connected_options.floatingip_id[idx]; resource_fip.push({ id: item.value, }) } return $scope.options.floatingips.concat(resource_fip); } return $scope.options.floatingips; } $scope.get_port_options = function(){ if ('port_id' in $scope.connected_options){ var resource_port = []; for (var idx in $scope.connected_options.port_id){ var item = $scope.connected_options.port_id[idx]; resource_port.push({ id: item.value, name: item.value }) } return $scope.options.ports.concat(resource_port); } return $scope.options.ports; } } osNeutronFloatingipAssociationController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNeutronFloatingipAssociationPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronFloatingipAssociationPath(basePath){ return basePath + 'js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronFloatingipAssociation', { templateUrl: osNeutronFloatingipAssociationPath, controller: osNeutronFloatingipAssociationController, bindings:{ 'floatingipassociation': '=', 'dependson': '=', 'formReference': '<', 'connectedoptions': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000027300000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000704313737340771034015 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000026700000000000011222 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingipassociation.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000410013737340771034004 0ustar zuulzuul00000000000000 {$ fip.id $}
    You must supply a floating IP.
    {$ port.name $}
    You must supply a port.
    Please input a valid IP address.
    ././@LongLink0000000000000000000000000000027000000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingipassociation/os__neutron__floatingassociation.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000573113737340771034017 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('os-neutron-floatingip-association', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_options({ floatingips: [{id: 'floatingip1-id', name: 'floatingip1-id'}], ports: [{id: 'port1-id', name: 'port1-id'}], }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''+ ''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with connectedoptions set', function() { $scope.resource = {floatingip_id: 'floatingip-id', port_id: 'port_id'} $scope.connectedoptions = {floatingip_id: [{value: 'floatingip-id'}], port_id: [{value: 'port-id'}], }; element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); }); })(); ././@LongLink0000000000000000000000000000021000000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000025200000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000334013737340771034011 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025300000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000334013737340771034011 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025200000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000421013737340771034006 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-security-group', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title Properties with rules undefined', function() { $scope.resource = {rules: []} element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('rules should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_rule(); expect($scope.resource.rules.length).toEqual(2); }); it('rules should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_rule(); expect($scope.resource.rules.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000024700000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001314213737340771034012 0ustar zuulzuul00000000000000
    You must supply an volume name.
    That doesn't look like a valid volume name.
    Too long volume name.
    Please do not input invalid characters!
    Please input the description less than 255 characters.
    ingress egress IPv4 IPv6 TCP UDP ICMP remote_ip_prefix remote_group_id
    It doesn't looks like a valid IP prefix!
    ././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000567113737340771034022 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::SecurityGroup angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronSecurityGroupSettings', { resource_key: "OS__Neutron__SecurityGroup", admin: false, icon: { class: 'fa-shield ', name: 'OS::Neutron::SecurityGroup', code: '\uf132', color: '#40a5f2' }, label: 'name', modal_component: '', edge_settings: null, necessary_properties: null } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronSecurityGroupSettings', 'hotgenGlobals', function(osNeutronSecurityGroupSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronSecurityGroupSettings.resource_key, osNeutronSecurityGroupSettings.icon); hotgenGlobals.update_resource_components( osNeutronSecurityGroupSettings.resource_key, osNeutronSecurityGroupSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronSecurityGroupSettings.resource_key, osNeutronSecurityGroupSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronSecurityGroupSettings.resource_key, osNeutronSecurityGroupSettings.label); }]); function osNeutronSecurityGroupController($scope, validationRules) { this.$onInit = function(){ if (typeof this.securitygroup.rules === 'undefined'){ this.securitygroup.rules = [{}]; } $scope.dependson = this.dependson; } this.add_rule = function(){ this.securitygroup.rules.push({}) } this.delete_rule = function(index){ this.securitygroup.rules.splice(index, 1) } $scope.validate_name = validationRules['name']; $scope.validate_cidr = validationRules['cidr']; } osNeutronSecurityGroupController.$inject = ['$scope', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules',]; osNeutronSecurityGroupPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronSecurityGroupPath(basePath){ return basePath + 'js/resources/os__neutron__securitygroup/os__neutron__securitygroup.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronSecurityGroup', { templateUrl: osNeutronSecurityGroupPath, controller: osNeutronSecurityGroupController, bindings:{ 'securitygroup': '=', 'dependson': '=', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000333413737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025400000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000333413737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000025300000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__securitygroup/os__neutron__securitygroup-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000334013737340771034011 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000020500000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000421213737340771034010 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024100000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001215013737340771034010 0ustar zuulzuul00000000000000 {$ fnet.name $}
    You must supply an floating network.
    Please input a valid IP address.
    Please input a valid IP address.
    {$ 'Show More Properties' | translate $}
    {$ fsubnet.name $} {$ port.name $}
    Please input a valid DNS Domain.
    Please input a valid DNS Name.
    ././@LongLink0000000000000000000000000000023700000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001574113737340771034021 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; /* OS::Neutron::FloatingIP */ angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronFloatingipSettings', { resource_key: "OS__Neutron__FloatingIP", admin: false, icon: { class: 'fa-neuter', name: 'OS::Neutron::FloatingIP', code: '\uf22c', color: '#40a5f2' }, label: 'floating_ip_address', modal_component: '', edge_settings: { 'OS__Neutron__Net': { 'type': 'property', 'property': 'floating_network', 'limit': 1, }, 'OS__Neutron__Port': { 'type': 'property', 'property': 'port_id', 'limit': 1, }, 'OS__Neutron__Subnet': { 'type': 'property', 'property': 'floating_subnet', 'limit': 1, }, }, necessary_properties: { 'floating_network': ['OS__Neutron__Net'] }, } ) angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronFloatingipSettings', 'hotgenGlobals', function(osNeutronFloatingipSettings, hotgenGlobals){ hotgenGlobals.update_node_labels( osNeutronFloatingipSettings.resource_key, osNeutronFloatingipSettings.label); hotgenGlobals.update_resource_icons( osNeutronFloatingipSettings.resource_key, osNeutronFloatingipSettings.icon); hotgenGlobals.update_resource_components( osNeutronFloatingipSettings.resource_key, osNeutronFloatingipSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronFloatingipSettings.resource_key, osNeutronFloatingipSettings.edge_settings); }]); function osNeutronFloatingipController($scope, hotgenGlobals, validationRules){ this.$onInit = function(){ if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } $scope.dependson = this.dependson; this.disable = { 'floating_network': false, 'floating_subnet': false, 'port_id': false } $scope.update = { floating_networks: $scope.get_floating_network_options(), floating_subnets: $scope.get_floating_subnet_options(), ports: $scope.get_port_options(), } if (typeof this.floatingip.value_specs == 'undefined'){ this.floatingip.value_specs = [{}] } if ( $scope.connected_options.floating_network && $scope.connected_options.floating_network.length > 0){ this.floatingip['floating_network'] = $scope.connected_options.floating_network[0].value this.disable.floating_network = true } if ( $scope.connected_options.floating_subnet && $scope.connected_options.floating_subnet.length > 0){ this.floatingip['floating_subnet'] = $scope.connected_options.floating_subnet[0].value this.disable.floating_subnet = true } if ( $scope.connected_options.port_id && $scope.connected_options.port_id.length > 0){ this.floatingip['port_id'] = $scope.connected_options.port_id[0].value this.disable.port_id = true } } $scope.show_more = false; $scope.validate_ip_address = validationRules['ip_address']; $scope.options = hotgenGlobals.get_resource_options(); this.add_value_specs = function(){ this.floatingip.value_specs.push({}) } this.delete_value_specs = function(index){ this.floatingip.value_specs.splice(index, 1) } $scope.get_floating_network_options = function(){ if ('floating_network' in $scope.connected_options){ var resource_floating_network = []; for (var idx in $scope.connected_options.floating_network){ var item = $scope.connected_options.floating_network[idx]; resource_floating_network.push({ id: item.value, name: item.value }) } return $scope.options.floating_networks.concat(resource_floating_network); } return $scope.options.floating_networks; } $scope.get_floating_subnet_options = function(){ if ('floating_subnet' in $scope.connected_options){ var resource_floating_subnet = []; for (var idx in $scope.connected_options.floating_subnet){ var item = $scope.connected_options.floating_subnet[idx]; resource_floating_subnet.push({ id: item.value, name: item.value }) } return $scope.options.floating_subnets.concat(resource_floating_subnet); } return $scope.options.floating_subnets; } $scope.get_port_options = function(){ if ('port_id' in $scope.connected_options){ var resource_port = []; for (var idx in $scope.connected_options.port_id){ var item = $scope.connected_options.port_id[idx]; resource_port.push({ id: item.value, name: item.value }) } return $scope.options.ports.concat(resource_port); } return $scope.options.ports; } } osNeutronFloatingipController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNeutronFloatingipPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronFloatingipPath(basePath){ return basePath + 'js/resources/os__neutron__floatingip/os__neutron__floatingip.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('osNeutronFloatingip', { templateUrl: osNeutronFloatingipPath, controller: osNeutronFloatingipController, bindings:{ 'floatingip': '=', 'dependson': '=', 'formReference': '<', 'connectedoptions': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000420613737340771034013 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000421213737340771034010 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000421213737340771034010 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024000000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000420613737340771034013 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000024400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__floatingip/os__neutron__floatingip.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000707313737340771034020 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-floatingip', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_options({ floating_networks: [{id: 'floating-network1-id', name: 'floating-network1-id'}], floating_subnets: [{id: 'floating-subnet1-id', name: 'floating-subnet1-id'}], ports: [{id: 'port1-id', name: 'port1-id'}], }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with connectedoptions set', function() { $scope.resource = {floating_network: 'floating-network-id', floating_subnet: 'floating-subnet-id', port_id: 'port_id'} $scope.connectedoptions = {floating_network: [{value: 'floating-network-id'}], floating_subnet: [{value: 'floating-subnet-id'}], port_id: [{value: 'port-id'}], }; element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('value_spec should be successfully added', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.add_value_specs(); expect($scope.resource.value_specs.length).toEqual(2); }); it('value_spec should be successfully deleted', function() { var $ctrl = element.isolateScope().$ctrl; $ctrl.delete_value_specs(); expect($scope.resource.value_specs.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000020100000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000023600000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000250613737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023100000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001440513737340771034015 0ustar zuulzuul00000000000000
    That doesn't look like a valid network name.
    Too long network name.
    {$ nw.name $}
    That doesn't look like a valid ip address.
    {$ subnet.name $}
    Admin State UP Enable SNAT
    {$ 'Show More Properties' | translate $}
    Distributed HA
    ././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000251213737340771034011 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023400000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000752313737340771034020 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-neutron-router', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_options({ networks: [{id: 'network1-id', name: 'network1-id'}], subnets: [{id: 'subnet1-id', name: 'subnet-id'}] }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with connectedoptions set', function(){ $scope.resource = {'external_gateway_info.network': 'network-id', 'ext_fixed_ips.subnet': ['subnet-id'],} $scope.connectedoptions = {'external_gateway_info.network': [{value: 'network-id'}], 'ext_fixed_ips.subnet': [{value: 'subnet-id'}], }; element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); it('value_spec should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_value_specs(); expect($scope.resource.value_specs.length).toEqual(2); }); it('value_spec should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_value_specs(); expect($scope.resource.value_specs.length).toEqual(0); }); it('external_fixed_ip should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_external_fixed_ip(); expect($scope.resource.external_gateway_info.external_fixed_ips.length).toEqual(2); }); it('external_fixed_ip should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_external_fixed_ip(); expect($scope.resource.external_gateway_info.external_fixed_ips.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000250613737340771034014 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023500000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000251213737340771034011 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023500000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000251213737340771034011 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__neutron__router/os__neutron__router.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001552713737340771034023 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Neutron::Router angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNeutronRouterSettings', { resource_key: "OS__Neutron__Router", // admin: true, icon: { class: 'fa-life-bouy', name: 'OS::Neutron::Router', code: '\uf1cd', color: '#40a5f2' }, label: 'name', modal_component: '', edge_settings: { 'OS__Neutron__Subnet': { 'type': 'property', 'property': 'ext_fixed_ips.subnet', 'limit': 99, }, 'OS__Neutron__Net': { 'type': 'property', 'property': 'external_gateway_info.network', 'limit': 1, }, }, necessary_properties: null } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNeutronRouterSettings', 'hotgenGlobals', function(osNeutronRouterSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNeutronRouterSettings.resource_key, osNeutronRouterSettings.icon); hotgenGlobals.update_node_admin( osNeutronRouterSettings.resource_key, osNeutronRouterSettings.admin); hotgenGlobals.update_resource_components( osNeutronRouterSettings.resource_key, osNeutronRouterSettings.modal_component); hotgenGlobals.update_edge_directions( osNeutronRouterSettings.resource_key, osNeutronRouterSettings.edge_settings); hotgenGlobals.update_node_labels( osNeutronRouterSettings.resource_key, osNeutronRouterSettings.label); }]); function osNeutronRouterController($scope, hotgenGlobals, validationRules) { this.$onInit = function(){ if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } if (typeof this.router.external_gateway_info === 'undefined'){ this.router.external_gateway_info = {"external_fixed_ips": [{}]}; } if (typeof this.router.l3_agent_ids === 'undefined'){ this.router.l3_agent_ids = []; } if (typeof this.router.tags == 'undefined'){ this.router.tags = [] } if (typeof this.router.admin_state_up === 'undefined'){ this.router.admin_state_up = true; } if (typeof this.router.value_specs == 'undefined'){ this.router.value_specs = [{}] } this.disable = { 'subnets': [], 'network': false, } if ( $scope.connected_options['ext_fixed_ips.subnet'] && $scope.connected_options['ext_fixed_ips.subnet'].length > 0){ for (var idx in $scope.connected_options['ext_fixed_ips.subnet']){ this.router.external_gateway_info.external_fixed_ips.splice(0,0, {subnet: $scope.connected_options['ext_fixed_ips.subnet'][idx].value}); this.disable.subnets.push($scope.connected_options['ext_fixed_ips.subnet'][idx].value); } } if ( $scope.connected_options['external_gateway_info.network'] && $scope.connected_options['external_gateway_info.network'].length > 0){ this.router.external_gateway_info['network'] = $scope.connected_options['external_gateway_info.network'][0].value; this.disable.network = true } $scope.update = {subnets: $scope.get_subnets_options(), networks: $scope.get_networks_options()} $scope.dependson = this.dependson; }; $scope.options = hotgenGlobals.get_resource_options(); $scope.show_more = false; $scope.validate_name = validationRules['name']; $scope.validate_ip_address = validationRules['ip_address']; this.add_external_fixed_ip = function(){ this.router.external_gateway_info.external_fixed_ips.push({}) } this.delete_external_fixed_ip = function(index){ this.router.external_gateway_info.external_fixed_ips.splice(index, 1) } this.add_value_specs = function(){ this.router.value_specs.push({}) } this.delete_value_specs = function(index){ this.router.value_specs.splice(index, 1) } $scope.get_networks_options = function(){ if ('external_gateway_info.network' in $scope.connected_options){ var resource_networks = []; for (var idx in $scope.connected_options['external_gateway_info.network']){ var item = $scope.connected_options['external_gateway_info.network'][idx]; resource_networks.push({ id: item.value, name: item.value }) } return $scope.options.networks.concat(resource_networks); } return $scope.options.networks; } $scope.get_subnets_options = function(){ if ('ext_fixed_ips.subnet' in $scope.connected_options){ var resource_subnets = []; for (var idx in $scope.connected_options['ext_fixed_ips.subnet']){ var item = $scope.connected_options['ext_fixed_ips.subnet'][idx]; resource_subnets.push({ id: item.value, name: item.value }) } return $scope.options.subnets.concat(resource_subnets); } return $scope.options.subnets; } } osNeutronRouterController.$inject = ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNeutronRouterPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNeutronRouterPath(basePath){ return basePath + 'js/resources/os__neutron__router/os__neutron__router.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNeutronRouter', { templateUrl: osNeutronRouterPath, controller: osNeutronRouterController, bindings:{ 'router': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000017600000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000377313737340771034023 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022600000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000003175013737340771034017 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-nova-server', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals, hotgenUtils; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); hotgenGlobals = $injector.get('hotgenGlobals'); hotgenUtils = $injector.get('hotgenUtils'); hotgenGlobals.update_resource_options({ networks: [{id: 'network1-id', name: 'network1-id'}], subnets: [{id: 'subnet1-id', name: 'subnet1-id'}], ports: [{id: 'port1-id', name: 'port1-id'}], volumes: [{id: 'volume1-id', name: 'volume1-id'}], floatingips: [{id: 'ipaddress', name: 'ipaddress'}], security_groups: [{id: 'secgroup-id1', name: 'secgroup-id1'}], keypairs: [{id: 'keyname1', name: 'keyname1'}], }); $scope.resource = {}; $scope.dependson = []; $scope.connectedoptions = []; $scope.resourceForm = {}; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with no connectedoptions set', function() { element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with connectedoptions set', function(){ $scope.resource = {image: 'image-id', key_name: 'keyname', security_groups: ['secgroup-01'], block_device_mapping_v2: [ {volume_id: '{ get_resource: vol01 }'}, {image: 'image-id'}, {image: '{ get_resource: image01 }'}, {snapshot_id: 'snapshot-id'}, {ignore: 'ignore'} ], networks: [{network: '{ get_resource: network01 }'}, {subnet: '{ get_resource: subnet01 }'}, {port: '{ get_resource: port01 }'}, {floating_ip: '{ get_resource: floatingip01 }'}, {ignore: 'ignore'} ], } $scope.connectedoptions = { 'networks.network': [{value: 'network-id'}, {value: '{ get_resource: network01 }'}], 'networks.subnet': [{value: 'subnet-id'}, {value: '{ get_resource: subnet01 }'}], 'networks.port': [{value: 'port-id'}, {value: '{ get_resource: port01 }'}], 'networks.floating_ip': [{value: 'floatingip'}, {value: '{ get_resource: floatingip01 }'}], key_name: 'keyname', security_groups: [{value: 'secgroup-id'}], 'block_device_mapping_v2.volume_id': [{value: 'volume-id'}, {value: '{ get_resource: vol01 }'}], } element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); $isolateScope.update_boot_source(); for (var i in $scope.resource.block_device_mapping_v2){ $isolateScope.update_source(i); } for (var i in $scope.resource.networks){ $isolateScope.update_nwconfig(i); } expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with connectedoptions set with error', function(){ $scope.resource = {image: 'image-id', key_name: 'keyname', security_groups: ['secgroup-01'], block_device_mapping_v2: [ {volume_id: '{ get_resource: vol01 }'}, {image: 'image-id'}, {image: '{ get_resource: image01 }'}, {snapshot_id: 'snapshot-id'}, {ignore: 'ignore'} ], networks: [{network: '{ get_resource: network01 }'}, {subnet: '{ get_resource: subnet01 }'}, {port: '{ get_resource: port01 }'}, {floating_ip: '{ get_resource: floatingip01 }'} ], } $scope.connectedoptions = { 'networks.network': [{value: 'network-id'}, {value: '{ get_resource: network01 }'}], 'networks.subnet': [{value: 'subnet-id'}, {value: '{ get_resource: subnet01 }'}], 'networks.port': [{value: 'port-id'}, {value: '{ get_resource: port01 }'}], 'networks.floating_ip': [{value: 'floatingip'}, {value: '{ get_resource: floatingip01 }'}], key_name: 'keyname', security_groups: [{value: 'secgroup-id'}], 'block_device_mapping_v2.volume_id': [{value: 'volume-id'}, {value: '{ get_resource: vol01 }'}], } spyOn(hotgenUtils, 'filter_and_return_get_resource_element').and.callFake(function(){ return [{}] }); element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with boot from image snapshot', function(){ $scope.resource = {image_snapshot: 'image-id'} element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); $isolateScope.update_boot_source(); expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with boot from volume ', function(){ $scope.resource = {volume: 'vol-id'} element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); $isolateScope.update_boot_source(); expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with boot from volume snapshot', function(){ $scope.resource = {volume_snapshot: 'vol-id'} element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); $isolateScope.update_boot_source(); expect(element.find('span').html()).toContain("Basic"); }); it('find tab title with boot from nothing ', function(){ $scope.resource = {ignore: 'nothing'} element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); $isolateScope.update_boot_source(); expect(element.find('span').html()).toContain("Basic"); }); it('$scope.show_passwd should be successfully watched', function() { $isolateScope.show_passwd = true; $isolateScope.$digest(); expect($isolateScope.show_passwd_type).toEqual('text'); $isolateScope.show_passwd = false; $isolateScope.$digest(); expect($isolateScope.show_passwd_type).toEqual('password'); }); it('metadata should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_metadata(); expect($scope.resource.metadata.length).toEqual(2); }); it('metadata should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_metadata(); expect($scope.resource.metadata.length).toEqual(0); }); it('personality should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_personality(); expect($scope.resource.personality.length).toEqual(2); }); it('personality should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_personality(); expect($scope.resource.personality.length).toEqual(0); }); it('block_device_mapping should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_block_device_mapping(); expect($scope.resource.block_device_mapping.length).toEqual(1); }); it('block_device_mapping should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_block_device_mapping(); $ctrl.delete_block_device_mapping(0); expect($scope.resource.block_device_mapping.length).toEqual(0); }); it('block_device_mapping_v2 should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_block_device_mapping_v2(); expect($scope.resource.block_device_mapping_v2.length).toEqual(1); }); it('block_device_mapping_v2 should be successfully deleted', function() { $scope.resource = {block_device_mapping_v2: [{volume_id: 'vol-01'}]} element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); var $ctrl = $isolateScope.$ctrl; $ctrl.delete_block_device_mapping_v2(0); expect($scope.resource.block_device_mapping_v2.length).toEqual(0); }); it('networks should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_networks(); expect($scope.resource.networks.length).toEqual(1); }); it('networks should be successfully deleted', function() { $scope.resource = {networks: [{network: 'network-01'}]}; element = $compile(angular.element(''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); var $ctrl = $isolateScope.$ctrl; $ctrl.delete_networks(0); expect($scope.resource.networks.length).toEqual(0); }); it('scheduler_hints should be successfully added', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.add_scheduler_hints(); expect($scope.resource.scheduler_hints.length).toEqual(2); }); it('scheduler_hints should be successfully deleted', function() { var $ctrl = $isolateScope.$ctrl; $ctrl.delete_scheduler_hints(); expect($scope.resource.scheduler_hints.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000022200000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000377513737340771034025 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022700000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000400113737340771034004 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022100000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000006510013737340771034013 0ustar zuulzuul00000000000000(function(angular) { 'use strict'; // OS::Nova::Server angular.module('horizon.dashboard.project.heat_dashboard.template_generator').value('osNovaServerSettings', { resource_key: "OS__Nova__Server", admin: false, icon: { class: 'fa-desktop ', name: 'OS::Nova::Server', code: '\uf108', color: '#483dff' }, label: 'name', modal_component: '', edge_settings: { 'OS__Cinder__Volume': { 'type': 'property', 'property': 'block_device_mapping_v2.volume_id', 'limit': 99, 'occupied': true, 'lonely': true, }, 'OS__Nova__KeyPair': { 'type': 'property', 'property': 'key_name', 'limit': 1, }, 'OS__Neutron__Net': { 'type': 'property', 'property': 'networks.network', 'limit': 99, }, 'OS__Neutron__FloatingIP': { 'type': 'property', 'property': 'networks.floating_ip', 'limit': 99, }, 'OS__Neutron__Subnet': { 'type': 'property', 'property': 'networks.subnet', 'limit': 99, }, 'OS__Neutron__Port': { 'type': 'property', 'property': 'networks.port', 'limit': 99, }, 'OS__Neutron__SecurityGroup': { 'type': 'property', 'property': 'security_groups', 'limit': 99, }, }, necessary_properties: { 'flavor': null } } ); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osNovaServerSettings', 'hotgenGlobals', function(osNovaServerSettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osNovaServerSettings.resource_key, osNovaServerSettings.icon); hotgenGlobals.update_node_labels( osNovaServerSettings.resource_key, osNovaServerSettings.label); hotgenGlobals.update_resource_components( osNovaServerSettings.resource_key, osNovaServerSettings.modal_component); // for (var i in osNovaServerSettings.edge_settings){ // if (osNovaServerSettings.edge_settings[i].modal){ // hotgenGlobals.update_resource_components( // osNovaServerSettings.resource_key+'_'+i, // osNovaServerSettings.edge_settings[i].modal); // } // } hotgenGlobals.update_edge_directions( osNovaServerSettings.resource_key, osNovaServerSettings.edge_settings); }]); function osNovaServerController($scope, hotgenGlobals, hotgenNotify, hotgenUtils, validationRules) { this.$onInit = function(){ if (typeof this.connectedoptions === 'undefined'){ $scope.connected_options = [] } else{ $scope.connected_options = this.connectedoptions; } if (typeof this.instance.metadata === 'undefined'){ this.instance.metadata = [{}]; } if (typeof this.instance.tags === 'undefined'){ this.instance.tags = []; } if (typeof this.instance.scheduler_hints === 'undefined'){ this.instance.scheduler_hints = [{}]; } if (typeof this.instance.personality === 'undefined'){ this.instance.personality = [{}]; } if (typeof this.instance.block_device_mapping === 'undefined'){ this.instance.block_device_mapping = []; } if (typeof this.instance.security_groups === 'undefined'){ this.instance.security_groups = []; } if (typeof this.instance.block_device_mapping_v2 === 'undefined'){ this.instance.block_device_mapping_v2 = []; } if (typeof this.instance.networks === 'undefined'){ this.instance.networks = []; } this.disable = { 'key_name': false, 'security_groups': [], 'block_device_mapping_v2.volume_id': [], 'networks': {}, } if (this.instance.image){ $scope.boot_source = 'image'; } else if (this.instance.image_snapshot){ $scope.boot_source = 'image_snapshot'; } else if (this.instance.volume){ $scope.boot_source = 'volume'; } else if (this.instance.volume_snapshot){ $scope.boot_source = 'volume_snapshot'; } if ( $scope.connected_options.key_name && $scope.connected_options.key_name.length > 0){ this.instance['key_name'] = $scope.connected_options.key_name[0].value; this.disable.key_name = true; } var old_array = hotgenUtils.filter_and_return_get_resource_element(this.instance['security_groups']); if ( $scope.connected_options.security_groups && $scope.connected_options.security_groups.length > 0){ for (var idx in $scope.connected_options.security_groups){ this.instance['security_groups'].push($scope.connected_options.security_groups[idx].value); this.disable.security_groups.push($scope.connected_options.security_groups[idx].value); } } var old_array = hotgenUtils.filter_and_return_get_resource_element(this.instance['block_device_mapping_v2'], 'volume_id'); var exist_volume_map = {} for (var idx in old_array){ if (old_array[idx].volume_id && old_array[idx].volume_id.length > 0 ){ exist_volume_map[old_array[idx].volume_id] = old_array[idx]; } } this.disable.length = 0; $scope.bdpv2_source = {}; if ( $scope.connected_options['block_device_mapping_v2.volume_id'] && $scope.connected_options['block_device_mapping_v2.volume_id'].length > 0){ for (var idx in $scope.connected_options['block_device_mapping_v2.volume_id']){ var connected_vol_id = $scope.connected_options['block_device_mapping_v2.volume_id'][idx].value; var item = exist_volume_map[connected_vol_id]; if (item == null){ item = {volume_id: connected_vol_id} } this.instance.block_device_mapping_v2.push(item); this.disable['block_device_mapping_v2.volume_id'].push(connected_vol_id); } } for (var idx in this.instance.block_device_mapping_v2){ var disk = this.instance.block_device_mapping_v2[idx]; var source = ''; if (disk.volume_id && disk.volume_id != '') { source = 'volume'; } else if (disk.image && disk.image != '') { source = 'image'; } else if (disk.snapshot_id && disk.snapshot_id != '') { source = 'volume_snapshot'; } $scope.bdpv2_source[idx.toString()] = source; } var old_net_array = hotgenUtils.filter_and_return_get_resource_element(this.instance['networks'], 'network'); var exist_network_map = {} for (var idx in old_net_array){ if (old_net_array[idx].network && old_net_array[idx].network.length > 0 ){ exist_network_map[old_net_array[idx].network] = old_net_array[idx]; } } var old_subnet_array = hotgenUtils.filter_and_return_get_resource_element(this.instance['networks'], 'subnet'); var exist_subnet_map = {} for (var idx in old_subnet_array){ if (old_subnet_array[idx].subnet && old_subnet_array[idx].subnet.length > 0 ){ exist_subnet_map[old_subnet_array[idx].subnet] = old_subnet_array[idx]; } } var old_port_array = hotgenUtils.filter_and_return_get_resource_element(this.instance['networks'], 'port'); var exist_port_map = {} for (var idx in old_port_array){ if (old_port_array[idx].port && old_port_array[idx].port.length > 0 ){ exist_port_map[old_port_array[idx].port] = old_port_array[idx]; } } var old_fip_array = hotgenUtils.filter_and_return_get_resource_element(this.instance['networks'], 'floating_ip'); var exist_fip_map = {} for (var idx in old_fip_array){ if (old_fip_array[idx].floating_ip && old_fip_array[idx].floating_ip.length > 0 ){ exist_fip_map[old_fip_array[idx].floating_ip] = old_fip_array[idx]; } } if ( $scope.connected_options['networks.network'] && $scope.connected_options['networks.network'].length > 0){ for (var idx in $scope.connected_options['networks.network']){ var connected_nw_id = $scope.connected_options['networks.network'][idx].value; var item = exist_network_map[connected_nw_id]; if (item == null){ item = {network: connected_nw_id} } this.disable.networks[this.instance.networks.length.toString()] = true; $scope.how2config_networks[this.instance.networks.length.toString()] = 'network'; this.instance.networks.push(item); } } if ( $scope.connected_options['networks.subnet'] && $scope.connected_options['networks.subnet'].length > 0){ for (var idx in $scope.connected_options['networks.subnet']){ var connected_subnet_id = $scope.connected_options['networks.subnet'][idx].value; var item = exist_subnet_map[connected_subnet_id]; if (item == null){ item = {subnet: connected_subnet_id} } this.disable.networks[this.instance.networks.length] = true; $scope.how2config_networks[this.instance.networks.length.toString()] = 'subnet'; this.instance.networks.push(item); } } if ( $scope.connected_options['networks.port'] && $scope.connected_options['networks.port'].length > 0){ for (var idx in $scope.connected_options['networks.port']){ var connected_port_id = $scope.connected_options['networks.port'][idx].value; var item = exist_port_map[connected_port_id]; if (item == null){ item = {port: connected_port_id} } this.disable.networks[this.instance.networks.length] = true; $scope.how2config_networks[this.instance.networks.length.toString()] = 'port'; this.instance.networks.push(item); } } if ( $scope.connected_options['networks.floating_ip'] && $scope.connected_options['networks.floating_ip'].length > 0){ for (var idx in $scope.connected_options['networks.floating_ip']){ var connected_fip_id = $scope.connected_options['networks.floating_ip'][idx].value; var item = exist_fip_map[connected_fip_id]; if (item == null){ item = {floating_ip: connected_fip_id} } this.disable.networks[this.instance.networks.length] = true; $scope.how2config_networks[this.instance.networks.length.toString()] = 'floating_ip'; this.instance.networks.push(item); } } for (var idx in this.instance.networks){ var netconfig = this.instance.networks[idx]; var source = ''; if (netconfig.network && netconfig.network != '') { source = 'network'; } else if (netconfig.subnet && netconfig.subnet != '') { source = 'subnet'; } else if (netconfig.port && netconfig.port != '') { source = 'port'; } else if (netconfig.floating_ip && netconfig.floating_ip != '') { source = 'floating_ip'; } $scope.how2config_networks[idx.toString()] = source; } $scope.update = { keypairs: $scope.get_keypairs_options(), networks: $scope.get_networks_options(), subnets: $scope.get_subnets_options(), floatingips: $scope.get_floatingips_options(), ports: $scope.get_ports_options(), security_groups: $scope.get_security_groups_options(), volumes: $scope.get_volumes_options(), } $scope.dependson = this.dependson; } $scope.show_passwd = false; $scope.show_passwd_type = "password"; $scope.bdpv2_source = {}; // Mark the source selected of every block_device_mapping_v2 item. $scope.how2config_networks = {} $scope.update_boot_source = function(){ if ($scope.boot_source == 'image'){ this.$ctrl.instance.image_snapshot = null; this.$ctrl.instance.volume = null; this.$ctrl.instance.volume_snapshot = null; } else if ($scope.boot_source == 'image_snapshot'){ this.$ctrl.instance.image = null; this.$ctrl.instance.volume = null; this.$ctrl.instance.volume_snapshot = null; } else if ($scope.boot_source == 'volume'){ this.$ctrl.instance.image = null; this.$ctrl.instance.image_snapshot = null; this.$ctrl.instance.volume_snapshot = null; } else if ($scope.boot_source == 'volume_snapshot'){ this.$ctrl.instance.image = null; this.$ctrl.instance.image_snapshot = null; this.$ctrl.instance.volume = null; } } $scope.update_source = function (index) { if ($scope.bdpv2_source[index] == 'volume'){ this.$ctrl.instance.block_device_mapping_v2[index].image = null; this.$ctrl.instance.block_device_mapping_v2[index].snapshot_id = null; } else if ($scope.bdpv2_source[index] == 'volume_snapshot'){ this.$ctrl.instance.block_device_mapping_v2[index].volume_id = null; this.$ctrl.instance.block_device_mapping_v2[index].image = null; } else if ($scope.bdpv2_source[index] == 'image'){ this.$ctrl.instance.block_device_mapping_v2[index].volume_id = null; this.$ctrl.instance.block_device_mapping_v2[index].snapshot_id = null; } } $scope.update_nwconfig = function (index) { if ($scope.how2config_networks[index] == 'network'){ this.$ctrl.instance.networks[index].subnet = null; this.$ctrl.instance.networks[index].port = null; this.$ctrl.instance.networks[index].floating_ip = null; } else if ($scope.how2config_networks[index] == 'subnet'){ this.$ctrl.instance.networks[index].network = null; this.$ctrl.instance.networks[index].port = null; this.$ctrl.instance.networks[index].floating_ip = null; } else if ($scope.how2config_networks[index] == 'port'){ this.$ctrl.instance.networks[index].subnet = null; this.$ctrl.instance.networks[index].network = null; this.$ctrl.instance.networks[index].floating_ip = null; } else if ($scope.how2config_networks[index] == 'floating_ip'){ this.$ctrl.instance.networks[index].subnet = null; this.$ctrl.instance.networks[index].port = null; this.$ctrl.instance.networks[index].network = null; } } $scope.$watch("show_passwd", function(newValue, oldValue) { $scope.show_passwd_type = $scope.show_passwd ? "text" : "password"; }); $scope.options = hotgenGlobals.get_resource_options(); $scope.get_security_groups_options = function(){ if ('security_groups' in $scope.connected_options){ var resource_secgroups = []; for (var idx in $scope.connected_options.security_groups){ var item = $scope.connected_options.security_groups[idx]; resource_secgroups.push({ id: item.value, name: item.value }) } return $scope.options.security_groups.concat(resource_secgroups); } return $scope.options.security_groups; } $scope.get_volumes_options = function(){ if ('block_device_mapping_v2.volume_id' in $scope.connected_options){ var resource_volumes = []; for (var idx in $scope.connected_options['block_device_mapping_v2.volume_id']){ var item = $scope.connected_options['block_device_mapping_v2.volume_id'][idx]; resource_volumes.push({ id: item.value, name: item.value }) } return $scope.options.volumes.concat(resource_volumes); } return $scope.options.volumes; } $scope.get_keypairs_options = function(){ if ('key_name' in $scope.connected_options){ var resource_keypair = []; for (var idx in $scope.connected_options.key_name){ var item = $scope.connected_options.key_name[idx]; resource_keypair.push({ name: item.value }) } return $scope.options.keypairs.concat(resource_keypair); } return $scope.options.keypairs; } $scope.get_networks_options = function(){ if ('networks.network' in $scope.connected_options){ var resource_nw = []; for (var idx in $scope.connected_options['networks.network']){ var item = $scope.connected_options['networks.network'][idx]; resource_nw.push({ id: item.value, name: item.value }) } return $scope.options.networks.concat(resource_nw); } return $scope.options.networks; } $scope.get_subnets_options = function(){ if ('networks.subnet' in $scope.connected_options){ var resource_subnet = []; for (var idx in $scope.connected_options['networks.subnet']){ var item = $scope.connected_options['networks.subnet'][idx]; resource_subnet.push({ id: item.value, name: item.value }) } return $scope.options.subnets.concat(resource_subnet); } return $scope.options.subnets; } $scope.get_floatingips_options = function(){ if ('networks.floating_ip' in $scope.connected_options){ var resource_fip = []; for (var idx in $scope.connected_options['networks.floating_ip']){ var item = $scope.connected_options['networks.floating_ip'][idx]; resource_fip.push({ id: item.value, }) } return $scope.options.floatingips.concat(resource_fip); } return $scope.options.floatingips; } $scope.get_ports_options = function(){ if ('networks.port' in $scope.connected_options){ var resource_port = []; for (var idx in $scope.connected_options['networks.port']){ var item = $scope.connected_options['networks.port'][idx]; resource_port.push({ id: item.value, name: item.value }) } return $scope.options.ports.concat(resource_port); } return $scope.options.ports; } $scope.block_device_mapping_v2 = true; $scope.deployment_swift_data = {}; $scope.show_more = false; $scope.options.boot_sources = [ {'id': 'image', 'name': 'image'}, {'id': 'image_snapshot', 'name': 'image snapshot'}, {'id': 'volume', 'name': 'volume'}, {'id': 'volume_snapshot', 'name': 'volume snapshot'} ]; $scope.options.volume_sources = [ {'id': 'volume', 'name': 'volume'}, {'id': 'volume_snapshot', 'name': 'volume snapshot'} ]; $scope.options.volume_sources_v2 = [ {'id': 'image', 'name': 'image'}, {'id': 'volume', 'name': 'volume'}, {'id': 'volume_snapshot', 'name': 'volume snapshot'} ]; $scope.options.flavor_update_policies = [ {'name': 'RESIZE', 'default': true}, {'name': 'REPLACE'}, ]; $scope.options.image_update_policies = [ {'name': 'REBUILD', 'default': true}, {'name': 'REPLACE'}, {'name': 'REBUILD_PRESERVE_EPHEMERAL'}, ]; $scope.options.disk_configs = [ {'name': 'AUTO', 'default': true}, {'name': 'MANUAL'}, ]; $scope.options.software_config_transports = [ {'name': 'POLL_SERVER_CFN', 'default': true}, {'name': 'POLL_SERVER_HEAT'}, {'name': 'POLL_TEMP_URL'}, {'name': 'ZAQAR_MESSAGE'}, ]; $scope.options.user_data_update_policies = [ {'name': 'REPLACE', 'default': true}, {'name': 'IGNORE'}, ]; $scope.options.user_data_formats = [ {'name': 'HEAT_CFNTOOLS', 'default': true}, {'name': 'RAW'}, {'name': 'SOFTWARE_CONFIG'} ]; $scope.options.disk_types = [ {'name': 'disk'}, {'name': 'cdrom'}, ] $scope.options.disk_buses = [ {'name': 'ide'}, {'name': 'lame_bus'}, {'name': 'scsi'}, {'name': 'usb'}, {'name': 'virtio'}, ]; $scope.options.ephemeral_formats = [ {'name': 'ext2'}, {'name': 'ext3'}, {'name': 'ext4'}, {'name': 'xfs'}, {'name': 'ntfs'}, ]; $scope.options.allocate_networks = [{'name': 'none'}, {'name': 'auto'}]; $scope.validate_name = validationRules['name']; $scope.validate_ip_address = validationRules['ip_address']; $scope.validate_uuid4 = validationRules['uuid4']; this.delete_metadata = function(index){ this.instance.metadata.splice(index, 1) } this.add_metadata = function(){ this.instance.metadata.push({}) } this.delete_personality = function(index){ this.instance.personality.splice(index, 1) } this.add_personality = function(){ this.instance.personality.push({}) } this.delete_block_device_mapping = function(index){ this.instance.block_device_mapping.splice(index, 1) } this.add_block_device_mapping = function(){ this.instance.block_device_mapping.push({}) } this.delete_block_device_mapping_v2 = function(index){ for (var i = index; i <= this.instance.block_device_mapping_v2.length; i=i+1){ $scope.bdpv2_source[i] = $scope.bdpv2_source[i+1]; } this.instance.block_device_mapping_v2.splice(index, 1) } this.add_block_device_mapping_v2 = function(){ this.instance.block_device_mapping_v2.push({}) } this.delete_networks = function(index){ for (var i = index; i < this.instance.networks.length; i=i+1){ $scope.how2config_networks[i] = $scope.how2config_networks[i+1]; } delete $scope.how2config_networks[this.instance.networks.length]; this.instance.networks.splice(index, 1); for (var i = index; i < this.instance.networks.length; i=i+1){ this.disable.networks[i] = this.disable.networks[i+1]; } delete this.disable.networks[this.instance.networks.length]; } this.add_networks = function(){ this.instance.networks.push({}) } this.delete_scheduler_hints = function(index){ this.instance.scheduler_hints.splice(index, 1) } this.add_scheduler_hints= function(){ this.instance.scheduler_hints.push({}) } } osNovaServerController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'hotgenUtils', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osNovaServerPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; function osNovaServerPath(basePath){ return basePath + 'js/resources/os__nova__server/os__nova__server.html'; } angular.module('horizon.dashboard.project.heat_dashboard.template_generator').component('osNovaServer', { templateUrl: osNovaServerPath, controller: osNovaServerController, bindings:{ 'instance': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })(window.angular); ././@LongLink0000000000000000000000000000022600000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000400113737340771034004 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000023000000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000377513737340771034025 0ustar zuulzuul00000000000000 Layer 1 ././@LongLink0000000000000000000000000000022300000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__nova__server/os__nova__server.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000006633513737340771034026 0ustar zuulzuul00000000000000
    That doesn't look like a valid instance name.
    Too long instance name.
    {$ flavor.name $}
    You must supply a flavor.
    {$ source.name $} {$ image.name $} {$ iss.name $} {$ vol.name $} {$ volss.name $} {$ az.name $} {$ 'Show More Properties' | translate $}
    {$ kname.name $} {$ sg.name $} Use block_device_mapping_v2
    {$ dtype.name $} {$ diskbus.name $}
    {$ eformat.name $} Delete On Termination
    {$ source.name $}
    {$ image.name $} {$ vol.name $} {$ volss.name $}
    {$ alloc_net.name $} network subnet port floating_ip
    {$ nw.name $} {$ subnet.name $} {$ port.name $} {$ fip.id $}
    Please input a valid IP address.
    Show Password
    {$ fup.name $} (Default) {$ iup.name $} (Default)
    That doesn't look like a valid uuid.
    Config Drive {$ dc.name $} (Default) {$ udf.name $} (Default) {$ sct.name $} (Default) {$ udup.name $} (Default)
    ././@LongLink0000000000000000000000000000020500000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000775000175000017500000000000013737341153034002 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000024400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000511513737340771034013 0ustar zuulzuul00000000000000 Layer 1Layer 2 P ././@LongLink0000000000000000000000000000024600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000510313737340771034010 0ustar zuulzuul00000000000000 Layer 1Layer 2 P ././@LongLink0000000000000000000000000000024000000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000511013737340771034006 0ustar zuulzuul00000000000000 Layer 1Layer 2 P ././@LongLink0000000000000000000000000000024400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000773313737340771034023 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('component os-heat-scalingpolicy', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $scope, $isolateScope, $compile; var hotgenGlobals; var element; beforeEach(inject(function($injector) { $scope = $injector.get('$rootScope').$new(); $compile = $injector.get('$compile'); $scope.resource = {}; $scope.dependson = []; $scope.resourceForm = {}; $scope.connectedoptions = []; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''+ ''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); $isolateScope = element.isolateScope(); })); it('find tab title Properties', function() { expect(element.find('span').html()).toContain("Properties"); }); it('find tab title with connectedoptions set', function() { $scope.resource = {auto_scaling_group_id: 'auto-scaling-group-uuid'} $scope.connectedoptions = {auto_scaling_group_id: [{value: 'auto-scaling-group-uuid'}], }; element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); expect($isolateScope.$ctrl.scalingpolicy.auto_scaling_group_id).toEqual('auto-scaling-group-uuid'); }); it('find tab title with auto_scaling_group_id set', function() { $scope.resource = {auto_scaling_group_id: '{ get_resource: AutoScalingGroup_1 }'} element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); expect($isolateScope.$ctrl.scalingpolicy.auto_scaling_group_id).toEqual(''); }); it('find tab title with adjustment type set', function() { $scope.resource = {adjustment_type: 'percent_change_in_capacity'} element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); $scope.resource = {adjustment_type: 'change_in_capacity'} element = $compile(angular.element(''+ ''))($scope); $scope.$digest(); $isolateScope = element.isolateScope(); expect(element.find('span').html()).toContain("Properties"); }); }); })(); ././@LongLink0000000000000000000000000000023700000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000001250113737340771034010 0ustar zuulzuul00000000000000(function() { 'use strict'; /* OS::Heat::ScalingPolicy * */ angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .value('osHeatScalingPolicySettings', { resource_key: "OS__Heat__ScalingPolicy", admin: false, icon: { class: 'fa-clone', name: 'OS::Heat::ScalingPolicy', code: '\uf24d', color: '#0bb238' }, label: 'name', modal_component: '', edge_settings: { 'OS__Heat__AutoScalingGroup': { 'type': 'property', 'property': 'auto_scaling_group_id', 'limit': 1, 'occupied': false, //* whether can be connected to any other resource */ 'lonely': false, //* whether can be connected to one more other resource */ 'modal': null } }, necessary_properties: ['adjustment_type', 'scaling_adjustment', 'auto_scaling_group_id'] } ) // Register the resource to globals angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .run(['osHeatScalingPolicySettings','hotgenGlobals', function(osHeatScalingPolicySettings, hotgenGlobals){ hotgenGlobals.update_resource_icons( osHeatScalingPolicySettings.resource_key , osHeatScalingPolicySettings.icon); hotgenGlobals.update_resource_components( osHeatScalingPolicySettings.resource_key, osHeatScalingPolicySettings.modal_component); hotgenGlobals.update_node_labels( osHeatScalingPolicySettings.resource_key, osHeatScalingPolicySettings.label); hotgenGlobals.update_edge_directions( osHeatScalingPolicySettings.resource_key, osHeatScalingPolicySettings.edge_settings); }]); // Define controller function osHeatScalingPolicyController($scope, hotgenGlobals, hotgenNotify, validationRules) { this.$onInit = function(){ $scope.dependson = this.dependson; $scope.min_adj_step_disabled = false; if (this.scalingpolicy.adjustment_type !=='percent_change_in_capacity'){ this.scalingpolicy.min_adjustment_step = ''; $scope.min_adj_step_disabled = true; } $scope.auto_scaling_group_id_disabled = false; $scope.auto_scaling_group_id_pattern = $scope.validate_uuid4; if (this.connectedoptions.auto_scaling_group_id){ for (var idx in this.connectedoptions.auto_scaling_group_id){ this.scalingpolicy.auto_scaling_group_id = this.connectedoptions.auto_scaling_group_id[idx].value; break; } $scope.auto_scaling_group_id_disabled = true; $scope.auto_scaling_group_id_pattern = ""; } else{ if (this.scalingpolicy.auto_scaling_group_id && this.scalingpolicy.auto_scaling_group_id.indexOf('get_resource') >= 0){ // no edge with AutoScalingGroup but still holding get_resource dependency, // consider edge is removed and empty auto_scaling_group_id this.scalingpolicy.auto_scaling_group_id = ""; } $scope.auto_scaling_group_id_disabled = false; } }; $scope.validate_integer = validationRules['integer']; $scope.validate_uuid4= validationRules['uuid4']; $scope.controller = this; $scope.adjustment_types = [ {'value': 'change_in_capacity', 'name': 'Change in Capacity'}, {'value': 'exact_capacity', 'name': 'Exact Capacity'}, {'value': 'percent_change_in_capacity', 'name': 'Percent Change in Capacity'}, ]; $scope.$watch('controller.scalingpolicy.adjustment_type', function(newValue, oldValue){ if (oldValue === newValue){ return; } if (newValue === 'percent_change_in_capacity'){ $scope.min_adj_step_disabled = false; } else{ $scope.controller.scalingpolicy.min_adjustment_step = ''; $scope.min_adj_step_disabled = true; } }); } function osHeatScalingPolicyPath (basePath){ return basePath + 'js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy.html'; } osHeatScalingPolicyController.$inject = ['$scope', 'hotgenGlobals', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.validationRules', ]; osHeatScalingPolicyPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('osHeatScalingpolicy', { templateUrl: osHeatScalingPolicyPath, controller: osHeatScalingPolicyController, bindings: { 'scalingpolicy': '=', 'dependson': '=', 'connectedoptions': '<', 'formReference': '<', } }); })();././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy-blue.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000511513737340771034013 0ustar zuulzuul00000000000000 Layer 1Layer 2 P ././@LongLink0000000000000000000000000000024100000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy.htmlheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000627313737340771034021 0ustar zuulzuul00000000000000 {$ at.name $}
    You did not enter a valid number.
    You must supply a scaling adjustment size.
    You must supply a valid ID.
    You must supply a auto scaling group ID.
    You did not enter a valid number.
    You did not enter a positive number.
    You did not enter a valid integer.
    You did not enter a positive number.
    You must supply an integer.
    ././@LongLink0000000000000000000000000000024500000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/resources/os__heat__scalingpolicy/os__heat__scalingpolicy-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/re0000664000175000017500000000512413737340771034013 0ustar zuulzuul00000000000000 Layer 1Layer 2 P ././@LongLink0000000000000000000000000000015600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000775000175000017500000000000013737341153033775 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000020100000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000407013737340771034005 0ustar zuulzuul00000000000000(function(){ 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', [ '$scope','hotgenAgent', 'hotgenGlobals', 'hotgenMessage', function($scope, hotgenAgent, hotgenGlobals, hotgenMessage){ $scope.template_versions = []; $scope.init = function(){ /* ********************************************************************* * The following selections should be replaced by OpenStack API response */ var optionsPromise = hotgenAgent.get_resource_options(); optionsPromise.then(function(options){ if (!options){ return; } hotgenGlobals.update_resource_options({ auth:{ tenant_id: '', admin: false }, keypair_types:[ {'name': 'ssh'}, {'name': 'x509'}, ], image_snapshots: [], floating_subnets: [], qos_policies: [], }); hotgenGlobals.update_resource_options(options); $scope.template_versions = hotgenGlobals.get_resource_options().template_versions hotgenMessage.broadcast_resources_loaded(); }); }; $scope.init(); $scope.update_template_version = function(template_version){ hotgenGlobals.set_template_version(template_version); return true; }; $scope.load_template_version = function(){ $scope.template_version = hotgenGlobals.get_template_version() } $scope.$on('update_template_version', $scope.load_template_version); }]) })(); ././@LongLink0000000000000000000000000000021100000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/dependson.directive.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000335413737340771034011 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator dependson directive', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var $compile, $rootScope, $scope, $isolateScope, element; beforeEach(inject(function($rootScope, $compile) { $scope = $rootScope.$new(); $scope.dependson = ['node-id-1111', 'node-id-2222', 'node-id-3333']; // element will enable you to test your directive's element on the DOM element = $compile(angular.element(''))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); // If the directive uses isolate scope, we need to get a reference to it // explicitly $isolateScope = element.isolateScope(); })); it('Replaces the element with the appropriate content', function() { expect(element.find('label').html()).toContain("Depends on"); }); it('toggle function modifies array', function() { var array = [0, 1, 2, 3, 4]; $isolateScope.toggle(5, array); expect(array.length).toEqual(6); $isolateScope.toggle(0, array); $isolateScope.toggle(5, array); expect(array.length).toEqual(4); }); it('check array item existence ', function() { var array = [0, 1, 2, 3, 4]; expect($isolateScope.exists(5, array)).toEqual(false); expect($isolateScope.exists(1, array)).toEqual(true); }); }); })(); ././@LongLink0000000000000000000000000000021200000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-template.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002135713737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.TempModalController', ['$scope', '$mdDialog', 'hotgenNotify', 'hotgenUtils', 'hotgenStates', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', 'horizon.dashboard.project.heat_dashboard.template_generator.projectPath', function($scope, $mdDialog, hotgenNotify, hotgenUtils, hotgenStates, hotgenGlobals, basePath, projectPath){ $scope.basePath = basePath; $scope.projectPath = projectPath; $scope.dialogController = function ($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify) { $scope.all_saved = false; $scope.cancel = function() { $mdDialog.cancel(); }; $scope.download = function() { // TODO: jump to stack creation page, or send to heat api. // Temporarily Download var today = new Date(); var filename = "template-"+today.toISOString(); var blob = new Blob([$scope.template_contents], {type: "text/plain;charset=utf-8"}); saveAs(blob, filename+".yaml.txt"); hotgenNotify.show_success('Template is downloaded.'); }; $scope.redirect = function(){ var redirect = window.location.origin + projectPath + 'stacks/'; window.location.href = redirect; }; $scope.save = function() { var files = hotgenGlobals.get_reference_files(); sessionStorage.setItem('files', JSON.stringify(files)); sessionStorage.setItem('generated', true); sessionStorage.setItem('template', $scope.template_contents); $scope.redirect(); // $mdDialog.cancel(); }; $scope.extract_properties = function(resource_data){ for (var property in resource_data){ var func = null; switch (property){ case 'description': case 'public_key': func = hotgenUtils.escape_characters; break; case 'metadata': case 'X-Account-Meta': case 'X-Container-Meta': case 'scheduler_hints': case 'value_specs': func = hotgenUtils.extract_keyvalue; break; case 'allocation_pools': case 'allowed_address_pairs': case 'block_device_mapping': case 'block_device_mapping_v2': case 'fixed_ips': case 'host_routes': case 'personality': case 'rules': func = hotgenUtils.extract_list_of_keyvalue; break; case 'dns_nameservers': case 'dhcp_agent_ids': case 'tags': func = hotgenUtils.extract_list; break; case 'resource_def': case 'resource': func = hotgenUtils.extract_resource_def; break; default: break; } if ( func != null){ resource_data[property] = func(resource_data[property]); } hotgenUtils.strip_property(resource_data, property); } return resource_data; } $scope.warning = "Warning: Some resources remain unsaved." $scope.generate = function(){ // Generate `resources` & `outputs` section var resource_root = {}; var outputs_root = {}; if( hotgenStates.get_saved_flags_length() == 0 || hotgenStates.get_saved_resources_length() == 0){ $scope.all_saved = false; $scope.warning = "Warning: Cannot generate, no resource has been saved."; return ''; } $scope.all_saved = hotgenStates.is_all_saved(); $scope.saved_resources = hotgenStates.get_saved_resources(); $scope.data = { nodes: hotgenStates.get_nodes(), } var depends_ons = hotgenStates.get_saved_dependsons(); for (var idkey in $scope.saved_resources){ // Generate `resources` section var resource_type = $scope.saved_resources[idkey].type; var resource_name = hotgenStates.get_label_by_uuid(idkey); var copy_data = angular.copy($scope.saved_resources[idkey].data) var properties = $scope.extract_properties(copy_data); resource_root[resource_name] = { type: resource_type.replace(/_/g, ':'), }; if (Object.keys(properties).length > 0){ resource_root[resource_name]["properties"] = properties; } if (idkey in depends_ons){ var depends_ids = angular.copy(depends_ons[idkey]); if (depends_ons[idkey].length > 0){ resource_root[resource_name]['depends_on'] = []; for (var idx in depends_ids){ resource_root[resource_name]['depends_on'].push(hotgenStates.get_label_by_uuid(depends_ids[idx])); } } } // Generate `outputs` section var output_detail = hotgenGlobals.get_resource_outputs(resource_type); for (var idx in output_detail){ var output_key = resource_name + '_' + output_detail[idx].property; outputs_root[output_key] = { description: 'The ' + output_detail[idx].property + ' of ' + resource_name +'.', value: '{ get_attr: ['+resource_name+', '+output_detail[idx].property+'] }', }; } } var today = new Date(); var template_version = hotgenGlobals.get_template_version().split('.')[1] var template_root = { heat_template_version: template_version, description: 'version 2017-09-01 created by HOT Generator at '+ today.toUTCString() + '.', resources: resource_root, } if (Object.keys(outputs_root).length > 0){ template_root['outputs'] = outputs_root } var json_string = JSON.stringify(template_root); return json2yaml(json_string); } $scope.template_contents = $scope.generate(); $scope.template = { title: 'Template', content: $scope.template_contents, }; } $scope.open = function(){ if (hotgenGlobals.get_template_version() == null){ hotgenNotify.show_error('Please select template version at first.'); return; } $scope.dialogController.$inject = ['$scope', '$mdDialog', 'hotgenStates', 'hotgenGlobals', 'hotgenNotify']; $mdDialog.show({ parent: angular.element(document.body), clickOutsideToClose:true, templateUrl: basePath+'templates/modal_template.html', controller: $scope.dialogController, }).then(function(){ // ; }, function(){ // ; }); }; }]); })(); ././@LongLink0000000000000000000000000000021000000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/loading.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000203513737340771034004 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('LoadingController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $controller, controller, $scope, $rootScope; beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.LoadingController', { $scope: $scope,}); })); beforeEach(inject(function(_$rootScope_) { $rootScope = _$rootScope_; })); it('should exist', function(){ expect(controller).toBeDefined(); }); it('loading is false by default', function(){ expect($scope.loading).toEqual(true); }); it('loading is true after message received', function(){ $rootScope.$broadcast('handle_resources_loaded'); expect($scope.loading).toEqual(false); }); }); })(); ././@LongLink0000000000000000000000000000020000000000000011205 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/globals.service.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000713313737340771034010 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('hotgen-utils') .service('hotgenGlobals', function () { var globals = { resource_icons: {}, edge_directions: {}, necessary_properties: {}, resource_components: {}, node_labels: {}, node_admin: {}, resource_options: {'auth': {'admin': false}}, template_version: null, resource_outputs: {}, reference_files: {}, }; return { get_element: function(ele){ return globals[ele]; }, get_node_labels: function () { return this.get_element('node_labels'); }, get_node_admin: function () { return this.get_element('node_admin'); }, get_resource_icons: function () { return this.get_element('resource_icons'); }, get_resource_components: function () { return this.get_element('resource_components'); }, get_edge_directions: function () { return this.get_element('edge_directions'); }, get_necessary_properties: function () { return this.get_element('necessary_properties'); }, update_node_labels: function(key, value) { globals.node_labels[key] = value; }, update_node_admin: function(key, value) { globals.node_admin[key] = value; }, update_resource_icons: function(key, value) { globals.resource_icons[key] = value; }, update_resource_components: function(key, value) { globals.resource_components[key] = value; }, update_edge_directions: function(key, value) { globals.edge_directions[key] = value; }, update_necessary_properties: function(key, value) { globals.necessary_properties[key] = value; }, get_resource_options: function(){ return this.get_element('resource_options'); }, update_resource_options: function(u_object){ angular.extend(globals.resource_options, u_object) }, set_template_version: function(value){ globals.template_version = value; }, get_template_version: function(){ return globals.template_version; }, set_resource_outputs: function(key, value){ globals.resource_outputs[key] = value }, get_resource_outputs: function(key){ return globals.resource_outputs[key]; }, get_resource_types: function(){ return Object.keys(globals.resource_icons); }, set_reference_file: function(key, value){ globals.reference_files[key] = value; }, get_reference_file: function(key){ return globals.reference_files[key]; }, get_reference_files: function(key){ return globals.reference_files; }, }; }); })();././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/icons.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000303513737340771034005 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.IconController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $controller, controller, $scope, hotgenGlobals; beforeEach(inject(function($injector){ hotgenGlobals = $injector.get('hotgenGlobals'); hotgenGlobals.update_resource_icons('OS__Project__Resource', ''); hotgenGlobals.update_resource_icons('OS__Key', ''); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.IconController', { $scope: $scope,}); })); it('should exist', function(){ expect(controller).toBeDefined(); }); it('check scope parameters', inject([ '$window', function($window){ var icons_number = Object.keys(hotgenGlobals.get_resource_icons()).length; expect(Object.keys($scope.resource_types).length).toEqual(icons_number); var admin_number = Object.keys(hotgenGlobals.get_node_admin()).length; expect(Object.keys($scope.resource_admin).length).toEqual(admin_number); expect($scope.admin).toEqual(false); expect($scope.basePath).toBe($window.STATIC_URL + 'dashboard/project/heat_dashboard/template_generator/'); }])); }); })(); ././@LongLink0000000000000000000000000000020200000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/utils.module.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002466013737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('hotgen-utils.hotgen-utils hotgenUUID', function(){ beforeEach(module('hotgen-utils')); var hotgenUUID; beforeEach(inject(function(_hotgenUUID_){ hotgenUUID = _hotgenUUID_; })); it('hotgenUUID should exist', function(){ expect(hotgenUUID).toBeDefined(); }); it('uuid length > 0', function(){ var returnValue = hotgenUUID.uuid(); expect(returnValue.length).toBeGreaterThan(0); }); }); describe('hotgen-utils.hotgen-utils hotgenNotify', function(){ beforeEach(module('hotgen-utils')); /* inject hotgenNotify */ var hotgenNotify; beforeEach(inject(function(_hotgenNotify_){ hotgenNotify = _hotgenNotify_; })); /* inject rootScope */ var $rootScope; beforeEach(inject(function(_$rootScope_) { $rootScope = _$rootScope_; })); it('should exist', function(){ expect(hotgenNotify).toBeDefined(); }); it('should show success', function(){ $rootScope.message_level = 4; var returnValue = hotgenNotify.show_success('success notify unit test!'); expect(returnValue).toEqual(0); }); it('should not show success', function(){ $rootScope.message_level = 0; var returnValue = hotgenNotify.show_success('success notify unit test!'); expect(returnValue).toEqual(-1); }); it('should show error', function(){ $rootScope.message_level = 4; var returnValue = hotgenNotify.show_error('fail notify unit test!'); expect(returnValue).toEqual(0); }); it('should not show error', function(){ $rootScope.message_level = -1; var returnValue = hotgenNotify.show_error('fail notify unit test!'); expect(returnValue).toEqual(-1); }); it('should show info', function(){ $rootScope.message_level = 4; var returnValue = hotgenNotify.show_info('info notify unit test!'); expect(returnValue).toEqual(0); }); it('should not show info', function(){ $rootScope.message_level = 0; var returnValue = hotgenNotify.show_info('info notify unit test!'); expect(returnValue).toEqual(-1); }); it('should show warning', function(){ $rootScope.message_level = 4; var returnValue = hotgenNotify.show_warning('warning notify unit test!'); expect(returnValue).toEqual(0); }); it('should not show warning', function(){ $rootScope.message_level = 0; var returnValue = hotgenNotify.show_warning('warning notify unit test!'); expect(returnValue).toEqual(-1); }); }); describe('hotgen-utils.hotgen-utils hotgenMessage', function(){ beforeEach(module('hotgen-utils')); var hotgenMessage; beforeEach(inject(function(_hotgenMessage_){ hotgenMessage = _hotgenMessage_; })); /* inject rootScope */ var $rootScope; beforeEach(inject(function(_$rootScope_) { $rootScope = _$rootScope_; spyOn($rootScope, '$broadcast'); })); it('hotgenMessage should exist', function(){ expect(hotgenMessage).toBeDefined(); }); it('should call event handle_edit_node', function(){ hotgenMessage.broadcast_edit_node('note_type'); expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_edit_node', 'note_type'); }); it('should call event handle_edit_edge', function(){ hotgenMessage.broadcast_edit_edge('from_type', 'to_type', 'from_id', 'to_id'); expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_edit_edge', { 'from_type': 'from_type', 'to_type': 'to_type', 'from_id': 'from_id', 'to_id': 'to_id', }); }); it('should call event handle_load_draft', function(){ hotgenMessage.broadcast_load_draft('note_type'); expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_load_draft'); }); it('should call event handle_resources_loaded', function(){ hotgenMessage.broadcast_resources_loaded(); expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_resources_loaded'); }); it('should call event update_template_version', function(){ hotgenMessage.broadcast_update_template_version(); expect($rootScope.$broadcast).toHaveBeenCalledWith('update_template_version'); }); }); describe('hotgen-utils.hotgen-utils hotgenUtils', function(){ beforeEach(module('hotgen-utils')); var hotgenUtils; beforeEach(inject(function(_hotgenUtils_){ hotgenUtils = _hotgenUtils_; })); it('hotgenUtils should exist', function(){ expect(hotgenUtils).toBeDefined(); }); it('should return string contains get_resource', function(){ var returnValue = hotgenUtils.get_resource_string('identity'); expect(returnValue).toEqual('{ get_resource: identity }'); }); it('should filter elements', function(){ var property = 'name'; var array = ['ignore me', 1, '{ get_resource: find me }', {'name': '{ get_resource: find me too }'}, {'no name': 'ignore me'}]; var returnValue = hotgenUtils.filter_and_return_get_resource_element(array, property); expect(returnValue.length).toEqual(2); }); it('should escape characters', function(){ var returnValue = hotgenUtils.escape_characters('replace " and \\ and \n '); expect(returnValue).toEqual('"replace \\" and \\\\ and \\n "'); }); it('should extrace keyvalue', function(){ var keyvalue = [{'key': 'key1', 'value': 'value1'}, {'key': 'key2', 'value': 'value2'}]; var returnValue = hotgenUtils.extract_keyvalue(keyvalue); expect(returnValue['key1']).toEqual('value1'); expect(returnValue['key2']).toEqual('value2'); keyvalue = [{}, {}]; returnValue = hotgenUtils.extract_keyvalue(keyvalue); expect(returnValue).toEqual(null); keyvalue = ''; returnValue = hotgenUtils.extract_keyvalue(keyvalue); expect(returnValue).toEqual(null); keyvalue = ['']; returnValue = hotgenUtils.extract_keyvalue(keyvalue); expect(returnValue).toEqual(null); }); it('should extract list of keyvalue', function(){ var keyvalue = []; var returnValue = hotgenUtils.extract_list_of_keyvalue(keyvalue); expect(returnValue).toEqual(null); keyvalue = 1; returnValue = hotgenUtils.extract_list_of_keyvalue(keyvalue); expect(returnValue).toEqual(null); keyvalue = [{'destination': 'destination', 'nexthop': 'nexthop'}, {}]; returnValue = hotgenUtils.extract_list_of_keyvalue(keyvalue); expect(returnValue.length).toEqual(1); keyvalue = [{}, {}]; returnValue = hotgenUtils.extract_list_of_keyvalue(keyvalue); expect(returnValue).toEqual(null); }); it('should extract list', function(){ var keyvalue = []; var returnValue = hotgenUtils.extract_list(keyvalue); expect(returnValue).toEqual(null); keyvalue = 1; returnValue = hotgenUtils.extract_list(keyvalue); expect(returnValue).toEqual(null); keyvalue = ['keep me', 'keep me too']; returnValue = hotgenUtils.extract_list(keyvalue); expect(returnValue.length).toEqual(2); }); it('should extract dicts', function(){ var keyvalue = {'discard me': '', 'discard me too': null}; var returnValue = hotgenUtils.extract_dicts(keyvalue); expect(Object.keys(returnValue).length).toEqual(0); keyvalue = {'keep me': [], 'keep me too': 'me'}; returnValue = hotgenUtils.extract_dicts(keyvalue); expect(Object.keys(returnValue).length).toEqual(2); }); it('should strip_property', function(){ var keyvalue = {'property': {'discard me': '', 'discard me too': null}}; hotgenUtils.strip_property(keyvalue, 'property'); expect(Object.keys(keyvalue).length).toEqual(0); keyvalue = {'property': {'keep me': [], 'keep me too': 'me'}}; hotgenUtils.strip_property(keyvalue, 'property'); expect(Object.keys(keyvalue).length).toEqual(1); keyvalue = {'property': {'keep me': [{}, []], 'keep me too': 'me'}}; hotgenUtils.strip_property(keyvalue, 'property'); expect(Object.keys(keyvalue).length).toEqual(1); expect(Object.keys(keyvalue['property']).length).toEqual(1); keyvalue = {'property': [null, '', {}, [] ]}; hotgenUtils.strip_property(keyvalue, 'property'); expect(Object.keys(keyvalue).length).toEqual(0); keyvalue = {'property': [1, 2, {'keep me': 3}, [4, 5, 6]]}; hotgenUtils.strip_property(keyvalue, 'property'); expect(Object.keys(keyvalue).length).toEqual(1); expect(Object.keys(keyvalue['property']).length).toEqual(4); keyvalue = {'property': [ ['', ''], [[[]]] ]}; hotgenUtils.strip_property(keyvalue, 'property'); expect(Object.keys(keyvalue).length).toEqual(0); }); it('should extract resource_def', function(){ var keyvalue = {'properties': [{'key': 'k1', 'value': 'v1'}], 'metadata': [{'key': 'k2', 'value': 'v2'}], 'type': 'some file'}; var returnValue = hotgenUtils.extract_resource_def(keyvalue); expect(returnValue.properties['k1']).toEqual('v1'); expect(returnValue.metadata['k2']).toEqual('v2'); var keyvalue = {'type': 'some file'}; var returnValue = hotgenUtils.extract_resource_def(keyvalue); expect(returnValue.type).toEqual('some file'); }); }); })(); ././@LongLink0000000000000000000000000000020400000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/states.service.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000001010513737340771034001 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('hotgen-utils.hotgenStates', function(){ beforeEach(module('hotgen-utils')); var hotgenStates; beforeEach(inject(function(_hotgenStates_){ hotgenStates = _hotgenStates_; })); it('should exist', function(){ expect(hotgenStates).toBeDefined(); }); it('check nodes and edges', function(){ expect(hotgenStates.get_nodes().length).toEqual(0); expect(hotgenStates.get_edges().length).toEqual(0); }); it('check network', function(){ expect(hotgenStates.get_network()).toEqual(null); var networkMock = {}; hotgenStates.set_network(networkMock); expect( hotgenStates.get_network().constructor).toEqual(Object); }); it('check increment counter/labels', function(){ expect(Object.keys(hotgenStates.get_incremented_labels()).length).toEqual(0); expect(Object.keys(hotgenStates.get_counters()).length).toEqual(0); expect(hotgenStates.increment_counter('type1')).toEqual(1); expect(hotgenStates.increment_counter('type1')).toEqual(2); expect(hotgenStates.get_counter('type1')).toEqual(2); hotgenStates.set_incremented_labels({}); hotgenStates.set_incremented_label('type1', 'type1_2'); expect(hotgenStates.get_label_by_uuid('type1')).toEqual('type1_2'); hotgenStates.set_counters({}); }); it('check saved_flags', function(){ expect(hotgenStates.get_saved_flags_length()).toEqual(0); expect(hotgenStates.is_all_saved()).toEqual(true); expect(Object.keys( hotgenStates.get_saved_flags()).length).toEqual(0); hotgenStates.set_saved_flags({'flag1': true}); expect(hotgenStates.is_all_saved()).toEqual(true); expect(hotgenStates.get_saved_flags_length()).toEqual(1); expect(hotgenStates.get_saved_flags()['flag1']).toEqual(true); hotgenStates.update_saved_flags('flag1', false); expect(hotgenStates.is_all_saved()).toEqual(false); expect(hotgenStates.get_saved_flags()['flag1']).toEqual(false); expect(hotgenStates.get_saved_flags_length()).toEqual(1); }); it('check dependsons', function(){ expect(Object.keys(hotgenStates.get_saved_dependsons()).length).toEqual(0); hotgenStates.set_saved_dependsons({'type1': ['resource1', 'resource2']}); expect( hotgenStates.get_saved_dependsons()['type1'].length).toEqual(2); hotgenStates.update_saved_dependsons('type2', ['resource3']); expect( hotgenStates.get_saved_dependsons()['type2'].length).toEqual(1); }); it('check saved resources', function(){ var returnValue = hotgenStates.get_saved_resources(); expect(Object.keys(returnValue).length).toEqual(0); expect(hotgenStates.get_saved_resources_length()).toEqual(0); hotgenStates.set_saved_resources({'resource_name': {'properties':{}}}); returnValue = hotgenStates.get_saved_resources(); expect(Object.keys(returnValue).length).toEqual(1); expect(hotgenStates.get_saved_resources_length()).toEqual(1); hotgenStates.update_saved_resources('resource_name', {'properties':{}}); hotgenStates.delete_saved_resources('resource_name'); returnValue = hotgenStates.get_saved_resources(); expect(Object.keys(returnValue).length).toEqual(0); expect(hotgenStates.get_saved_resources_length()).toEqual(0); }); it('check selected', function(){ var returnValue = hotgenStates.get_selected(); expect(Object.keys(returnValue).length).toEqual(0); hotgenStates.set_selected({'nodes': {}}); expect(Object.keys(hotgenStates.get_selected()).length).toEqual(1); }); it('check clear', function(){ hotgenStates.clear_states(); }); }); })(); ././@LongLink0000000000000000000000000000021400000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/vis-network.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002033013737340771034002 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.VisController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $controller, controller, $scope, $rootScope, hotgenGlobals, hotgenStates; beforeEach(inject(function($injector){ hotgenGlobals = $injector.get('hotgenGlobals'); hotgenStates = $injector.get('hotgenStates'); $rootScope = $injector.get('$rootScope'); spyOn($rootScope, '$broadcast'); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.VisController', { $scope: $scope,}); })); it('should exist', function(){ expect(controller).toBeDefined(); }); it('check scope parameters', function(){ expect($scope.data.nodes.length).toEqual(0); expect($scope.data.edges.length).toEqual(0); expect($scope.options.autoResize).toEqual(true); }); it('check $scope.get_node found', function(){ $scope.data.nodes.add({id: 'from_id'}); var returnValue = $scope.get_node('from_id'); expect(returnValue.id).toEqual('from_id'); }); it('check validate edge: invalid same node.', function(){ var data = {from: 'from_id', to: 'from_id'}; var returnValue = $scope.validate_edge(data); expect(returnValue).toEqual(false); }); it('check validate edge: connection allowed/ not allowed.', function(){ $scope.data.nodes.add({id: 'from_id', title: 'from_type'}); $scope.data.nodes.add({id: 'to_id', title: 'to_type'}); hotgenGlobals.update_edge_directions('from', {to: {}}); var data = {from: 'from_id', to: 'to_id'}; expect($scope.validate_edge(data)).toEqual(false); hotgenGlobals.update_edge_directions('from_type', {to: {}}); expect($scope.validate_edge(data)).toEqual(false); $scope.data.nodes.add({id: 'another_to_id', title: 'to_type'}); $scope.data.nodes.add({id: 'third_to_id', title: 'from_type'}); $scope.network = {getConnectedNodes: function(data){ return ['another_to_id', 'third_to_id']; }} hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 0}}); expect($scope.validate_edge(data)).toEqual(false); hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 10, lonely: true}}); expect($scope.validate_edge(data)).toEqual(false); hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 10, lonely: false, occupied: true}}); expect($scope.validate_edge(data)).toEqual(false); hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 10, lonely: false, occupied: false}}); expect($scope.validate_edge(data)).toEqual(true); $scope.data.nodes.update({id: 'third_to_id', title: 'another_type'}); hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 10, lonely: false, occupied: true}}); expect($scope.validate_edge(data)).toEqual(true); }); it('manipulation.addEdge: valid to add edge', function(){ var addEdgeFunction = $scope.options.manipulation.addEdge; var data = {from: 'from_id', to: 'to_id'}; var mockCallbackFunction = jasmine.createSpy().and.callFake(function() {return function(){}}) spyOn($scope, 'validate_edge').and.callFake(function(){return true}); addEdgeFunction(data, mockCallbackFunction); expect(mockCallbackFunction).toHaveBeenCalledWith(data); }); it('manipulation.addEdge: invalid to add edge', function(){ var addEdgeFunction = $scope.options.manipulation.addEdge; var data = {from: 'from_id', to: 'to_id'}; var mockCallbackFunction = jasmine.createSpy().and.callFake(function() {return function(){}}) spyOn($scope, 'validate_edge').and.callFake(function(){return false}); addEdgeFunction(data, mockCallbackFunction); expect(mockCallbackFunction).toHaveBeenCalledWith(null); }); it('manipulation.deleteNode', function(){ var deleteNodeFunction = $scope.options.manipulation.deleteNode; var data = {nodes: ['node_id'], edges:['edge_id']} var mockCallbackFunction = jasmine.createSpy().and.callFake(function() {return function(){}}) deleteNodeFunction(data, mockCallbackFunction); expect(mockCallbackFunction).toHaveBeenCalledWith(data); }); it('manipulation.deleteEdge', function(){ $scope.data.nodes.add({id: 'from_id', title: 'from_type'}); $scope.data.nodes.add({id: 'to_id', title: 'to_type'}); $scope.data.edges.add({id: 'edge_id', from: 'from_id', to: 'to_id'}); var deleteEdgeFunction = $scope.options.manipulation.deleteEdge; var data = {edges:['edge_id']} var mockCallbackFunction = jasmine.createSpy().and.callFake(function() {return function(){}}) deleteEdgeFunction(data, mockCallbackFunction); expect(mockCallbackFunction).toHaveBeenCalledWith(data); }); it('click nothing', function(){ var param = {edges: [], nodes: []} $scope.click(param); expect($rootScope.$broadcast).not.toHaveBeenCalled(); }); it('click to show node dialog', function(){ $scope.data.nodes.add({id: 'from_id', title: 'from_type'}); var param = {edges: [], nodes: ['from_id']} $scope.network = {disableEditMode: function(data){}} $scope.click(param); expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_edit_node', 'from_type'); }); it('click to show edge dialog', function(){ $scope.data.nodes.add({id: 'from_id', title: 'from_type'}); $scope.data.nodes.add({id: 'to_id', title: 'to_type'}); $scope.data.edges.add({id: 'edge_id', from: 'from_id', to: 'to_id'}); var param = {edges:['edge_id'], nodes:[]} $scope.network = {disableEditMode: function(data){}} $scope.click(param); var broadcast_param = { from_type: 'from_type', to_type: 'to_type', from_id: 'from_id', to_id: 'to_id', }; expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_edit_edge', broadcast_param); }); it('events on load', function(){ var network = {} spyOn(hotgenStates, 'set_network'); var onloadFunction = $scope.events.onload; onloadFunction(network); expect(hotgenStates.set_network).toHaveBeenCalledWith(network); }); it('get_added_edge_id: find the extra item', function(){ var old_ids = [1 ,2 ,3, 4]; var new_ids = [1, 2, 5, 3, 4]; var find = $scope.get_added_edge_id(old_ids, new_ids); expect(find).toEqual(5); }); it('get mapping', function(){ hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 10}}); expect($scope.get_mapping('from_type', 'to_type').limit).toEqual(10); expect($scope.get_mapping('from_type', 'to')).toEqual(false); expect($scope.get_mapping('from', 'to')).toEqual(false); }); it('get modal: found/not found', function(){ hotgenGlobals.update_edge_directions('from_type', {to_type: {limit: 10, modal: 'modal'}}); $scope.data.nodes.add({id: 'from_id', title: 'from_type'}); $scope.data.nodes.add({id: 'to_id', title: 'to_type'}); $scope.data.nodes.add({id: 'from', title: 'from'}); expect($scope.get_modal({from: 'from_id', to: 'to_id'})).toEqual('modal'); expect($scope.get_modal({from: 'from', to: 'to_id'})).toEqual(undefined); }) }); })(); ././@LongLink0000000000000000000000000000020000000000000011205 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/menu.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000673613737340771034020 0ustar zuulzuul00000000000000(function(){ 'use strict'; angular .module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.DraftMenuController', ['$scope', '$mdDialog', 'hotgenStates', 'hotgenGlobals', 'hotgenNotify', 'hotgenMessage', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify, hotgenMessage, basePath){ $scope.basePath = basePath; var originatorEv; $scope.openMenu = function($mdMenu, ev){ originatorEv = ev; $mdMenu.open(ev); }; $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), } $scope.save_draft = function(){ if ($scope.data.nodes.length == 0 && $scope.data.edges.length == 0){ hotgenNotify.show_warning('No resource to save.'); return; } if (localStorage.saved_counter) { localStorage.saved_counter = (Number(localStorage.saved_counter) + 1)%10; } else { localStorage.saved_counter = 0; } var data = { nodes: $scope.data.nodes._data, edges: $scope.data.edges._data, saved_resources: hotgenStates.get_saved_resources(), saved_depends_ons: hotgenStates.get_saved_dependsons(), is_saved: hotgenStates.get_saved_flags(), incremented_labels: hotgenStates.get_incremented_labels(), counter: hotgenStates.get_counters(), template_version: hotgenGlobals.get_template_version(), } var today = new Date(); data['time'] = today.toUTCString(); var drafts_serial = JSON.stringify(data); localStorage.setItem('draft_'+localStorage.saved_counter, drafts_serial); hotgenNotify.show_success('Draft is saved successfully at '+today.toUTCString()+'.'); } $scope.load_draft = function(){ hotgenMessage.broadcast_load_draft(); } $scope.import_draft = function(){ hotgenNotify.show_warning('Not Implemented.'); } $scope.export_draft = function(){ hotgenNotify.show_warning('Not Implemented.'); } }]); angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.ClearCanvasController', ['$scope', 'hotgenStates', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, hotgenStates, hotgenNotify, basePath){ $scope.basePath = basePath; $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), } $scope.clear_canvas = function(){ $scope.data.nodes.clear(); $scope.data.edges.clear(); hotgenStates.clear_states(); hotgenNotify.show_success('The Canvas has been initialized.'); }; }]); })(); ././@LongLink0000000000000000000000000000020400000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/dependson.directive.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000315513737340771034010 0ustar zuulzuul00000000000000(function(){ 'use strict'; /* Directive * */ function dependsonController($scope, hotgenStates, basePath){ this.$onInit = function(){ $scope.dependson = this.dependson; } $scope.basePath = basePath; $scope.edges = hotgenStates.get_edges(); $scope.nodes = hotgenStates.get_nodes(); $scope.selected = hotgenStates.get_selected(); $scope.toggle = function (item, list) { if (typeof item == 'undefined' || !(list instanceof Array)){ return; } var idx = list.indexOf(item); if (idx > -1) { list.splice(idx, 1); } else { list.push(item); } }; $scope.exists = function (item, list) { if (typeof item != "undefined" && list instanceof Array){ return list.indexOf(item) > -1; } return false; }; } function dependsonPath (basePath){ return basePath + 'templates/depends_on.html'; } dependsonPath.$inject = ['horizon.dashboard.project.heat_dashboard.template_generator.basePath']; dependsonController.$inject = [ '$scope', 'hotgenStates', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath']; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .component('dependsOn', { templateUrl: dependsonPath, controller: dependsonController, bindings:{ 'dependson': '=', } }); })(); ././@LongLink0000000000000000000000000000017700000000000011222 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/states.service.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000001276213737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('hotgen-utils') .service('hotgenStates', ['VisDataSet', function(VisDataSet){ // Caches for resources states. // { : true/false ...} var saved_flags = {}; var incremented_labels = {}; // { id: or } var selected = {}; // { : {: , ... } ...} var saved_resources = {}; // { : [ ...] ...} var saved_dependsons = {}; // Caches of canvas nodes and edges. var network = null; var nodes = new VisDataSet(); var edges = new VisDataSet(); var counter = {}; var increment_counter = function(type){ if (! (type in counter)){ counter[type] = 0; } counter[type] += 1; return counter[type]; } var get_counter = function(type){ return counter[type]; } var get_network = function(){ return network; } var set_network = function(visNetwork){ network = visNetwork } var get_nodes = function(){ return nodes; } var get_edges = function(){ return edges; } var get_selected = function(){ return selected; } var set_selected = function(to_set){ selected = to_set; } var update_saved_resources = function(key, data){ saved_resources[key] = data; }; var set_saved_resources = function(to_set){ saved_resources = to_set; } var update_saved_dependsons = function(key, data){ saved_dependsons[key] = data; }; var set_saved_dependsons = function(to_set){ saved_dependsons = to_set; } var is_all_saved = function(){ return(Object.values(saved_flags).indexOf(false) == -1); }; var get_saved_flags_length = function(){ return Object.keys(saved_flags).length; } var get_saved_flags = function(){ return saved_flags; } var set_saved_flags = function(to_set){ saved_flags = to_set; } var get_saved_dependsons = function(){ return saved_dependsons; } var update_saved_flags = function(key, value){ saved_flags[key] = value; } var get_saved_resources = function(){ return angular.copy(saved_resources); } var delete_saved_resources = function(key){ delete saved_resources[key]; } var get_saved_resources_length = function(){ return Object.keys(saved_resources).length; }; var clear_states = function(){ saved_resources = {} saved_dependsons = {} selected = {} saved_flags = {} incremented_labels = {} counter = {} } var get_label_by_uuid = function(key){ return incremented_labels[key]; } var set_incremented_label = function(key, label){ incremented_labels[key] = label } var get_incremented_labels = function(){ return incremented_labels; } var set_incremented_labels = function(labels){ incremented_labels = labels; } var get_counters = function(){ return counter; } var set_counters = function(cached){ counter = cached; } return { is_all_saved: is_all_saved, get_saved_resources: get_saved_resources, get_saved_resources_length: get_saved_resources_length, get_saved_dependsons: get_saved_dependsons, get_saved_flags: get_saved_flags, get_saved_flags_length: get_saved_flags_length, set_selected: set_selected, get_selected: get_selected, set_saved_resources: set_saved_resources, set_saved_dependsons: set_saved_dependsons, set_saved_flags: set_saved_flags, update_saved_flags: update_saved_flags, update_saved_resources: update_saved_resources, update_saved_dependsons: update_saved_dependsons, delete_saved_resources: delete_saved_resources, clear_states: clear_states, get_nodes: get_nodes, get_edges: get_edges, get_network: get_network, set_network: set_network, increment_counter: increment_counter, get_counter: get_counter, get_label_by_uuid: get_label_by_uuid, set_incremented_label: set_incremented_label, get_incremented_labels: get_incremented_labels, set_incremented_labels: set_incremented_labels, get_counters: get_counters, set_counters: set_counters, } }]) })(); ././@LongLink0000000000000000000000000000020500000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/menu.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000001063613737340771034012 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.DraftMenuController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $controller, controller, $scope, $rootScope, $mdMenu; beforeEach(inject(function($injector){ $rootScope = $injector.get('$rootScope'); $mdMenu = $injector.get('$mdMenu'); spyOn($rootScope, '$broadcast'); $mdMenu.open = jasmine.createSpy().and.callFake(function() { return function (callBack) { callBack(true); //return the value to be assigned. } }); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.DraftMenuController', { $scope: $scope,}); })); afterEach(function() { localStorage.clear(); }); it('DraftMenuController should exist', function(){ expect(controller).toBeDefined(); }); it('DraftMenuController basePath', inject([ '$window', function($window){ expect($scope.basePath).toBe($window.STATIC_URL + 'dashboard/project/heat_dashboard/template_generator/'); }])); it('scope.openMenu ', function(){ $scope.openMenu($mdMenu, {}); expect($mdMenu.open).toHaveBeenCalled(); }); it('DraftMenuController scope.data contains nodes and edges', function(){ expect($scope.data.nodes.length).toEqual(0); expect($scope.data.edges.length).toEqual(0); }); it('scope.save_draft stores in localStorage', function(){ expect(localStorage.length).toEqual(0); $scope.save_draft(); expect(localStorage.length).toEqual(0); $scope.data.nodes.add({'id': 'some-id'}); $scope.save_draft(); expect(localStorage.length).toEqual(2); $scope.data.nodes.add({'id': 'some-id-2'}); $scope.save_draft(); expect(localStorage.length).toEqual(3); }); it('scope.load_draft', function(){ $scope.load_draft(); expect($rootScope.$broadcast).toHaveBeenCalledWith('handle_load_draft'); }); it('scope.import_draft', function(){ $scope.import_draft(); }); it('scope.export_draft', function(){ $scope.export_draft(); }); }); describe('horizon.dashboard.project.heat_dashboard.template_generator.ClearCanvasController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $controller, controller, $scope, $rootScope; beforeEach(inject(function($injector){ $rootScope = $injector.get('$rootScope'); spyOn($rootScope, '$broadcast'); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.ClearCanvasController', { $scope: $scope,}); })); afterEach(function() { localStorage.clear(); }); it('ClearCanvasController should exist', function(){ expect(controller).toBeDefined(); }); it('ClearCanvasController basePath', inject([ '$window', function($window){ expect($scope.basePath).toBe($window.STATIC_URL + 'dashboard/project/heat_dashboard/template_generator/'); }])); it('ClearCanvasController scope.data contains nodes and edges', function(){ expect($scope.data.nodes.length).toEqual(0); expect($scope.data.edges.length).toEqual(0); }); it('scope.clear_canvas should empty $scope.data', function(){ $scope.data.nodes.add({'id': 'some-node-id'}); $scope.data.edges.add({'id': 'some-edge-id'}); expect($scope.data.nodes.length).toEqual(1); expect($scope.data.edges.length).toEqual(1); $scope.clear_canvas(); expect($scope.data.nodes.length).toEqual(0); expect($scope.data.edges.length).toEqual(0); }); }); })(); ././@LongLink0000000000000000000000000000020500000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/globals.service.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000751713737340771034016 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('hotgen-utils.hotgenGlobals', function(){ beforeEach(module('hotgen-utils')); var hotgenGlobals; beforeEach(inject(function(_hotgenGlobals_){ hotgenGlobals = _hotgenGlobals_; })); it('should exist', function(){ expect(hotgenGlobals).toBeDefined(); }); it('check get_element', function(){ var returnValue = hotgenGlobals.get_element('resource_icons'); expect(Object.keys(returnValue).length).toEqual(0); }); it('check node_labels', function(){ var returnValue = hotgenGlobals.get_node_labels(); expect(Object.keys(returnValue).length).toEqual(0); hotgenGlobals.update_node_labels('key1', 'label1') expect(Object.keys(returnValue).length).toEqual(1); }); it('check node_admin', function(){ var returnValue = hotgenGlobals.get_node_admin(); expect(Object.keys(returnValue).length).toEqual(0); hotgenGlobals.update_node_admin('key1', 'admin'); expect(Object.keys(returnValue).length).toEqual(1); }); it('check resource_icons', function(){ var returnValue = hotgenGlobals.get_resource_icons(); expect(Object.keys(returnValue).length).toEqual(0); hotgenGlobals.update_resource_icons('key1', 'admin'); expect(Object.keys(returnValue).length).toEqual(1); }); it('check get_resource_components', function(){ var returnValue = hotgenGlobals.get_resource_components(); expect(Object.keys(returnValue).length).toEqual(0); hotgenGlobals.update_resource_components('key1', 'component'); expect(Object.keys(returnValue).length).toEqual(1); }); it('check get_edge_directions', function(){ var returnValue = hotgenGlobals.get_edge_directions(); expect(Object.keys(returnValue).length).toEqual(0); hotgenGlobals.update_edge_directions('key1', 'edge'); expect(Object.keys(returnValue).length).toEqual(1); }); it('check get_template_version', function(){ var returnValue = hotgenGlobals.get_template_version(); expect(returnValue).toEqual(null); hotgenGlobals.set_template_version(['v1', 'v2']); expect(hotgenGlobals.get_template_version().length).toEqual(2); }); it('check get_necessary_properties', function(){ var returnValue = hotgenGlobals.get_necessary_properties(); expect(Object.keys(returnValue).length).toEqual(0); hotgenGlobals.update_necessary_properties('key1', ['p1', 'p2']); expect(hotgenGlobals.get_necessary_properties()['key1'].length).toEqual(2); }); it('check get_resource_types', function(){ var returnValue = hotgenGlobals.get_resource_types(); expect(Object.keys(returnValue).length).toEqual(0); }); it('check get_resource_options', function(){ var returnValue = hotgenGlobals.get_resource_options(); expect(Object.keys(returnValue).length).toEqual(1); hotgenGlobals.update_resource_options(['op1', 'op2']); expect(Object.keys(returnValue).length).toEqual(3); }); it('check get_resource_outputs', function(){ hotgenGlobals.set_resource_outputs('key1', ''); var returnValue = hotgenGlobals.get_resource_outputs('key1'); expect(returnValue).toEqual(''); }); it('check get_reference_file', function(){ hotgenGlobals.set_reference_file('key1', ''); var returnValue = hotgenGlobals.get_reference_file('key1'); expect(returnValue).toEqual(''); }); }); })(); ././@LongLink0000000000000000000000000000020700000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-draft.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000633313737340771034011 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.DraftModalController', ['$scope', '$mdDialog', 'hotgenNotify', 'hotgenMessage', 'hotgenStates', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, $mdDialog, hotgenNotify, hotgenMessage, hotgenStates, hotgenGlobals, basePath){ $scope.draftDialogController = function ($scope, $mdDialog, hotgenStates) { $scope.draft_list = []; $scope.latest_draft = JSON.parse(localStorage.getItem('draft_'+localStorage.saved_counter)); for (var i = 0 ; i < 10; i++){ if (localStorage.getItem('draft_'+i)){ var saved_drafts = JSON.parse(localStorage.getItem('draft_'+i)); $scope.draft_list.push(saved_drafts); } } $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), } $scope.load = function(draft) { $mdDialog.hide(); $scope.data.nodes.clear(); for (var id in draft.nodes){ $scope.data.nodes.add(draft.nodes[id]); } $scope.data.edges.clear(); for (var id in draft.edges){ $scope.data.edges.add(draft.edges[id]); } hotgenStates.set_saved_resources(draft.saved_resources); hotgenStates.set_saved_dependsons(draft.saved_depends_ons); hotgenStates.set_saved_flags(draft.is_saved); hotgenStates.set_counters(draft.counter); hotgenStates.set_incremented_labels(draft.incremented_labels); hotgenGlobals.set_template_version(draft.template_version); hotgenMessage.broadcast_update_template_version(); }; $scope.cancel = function() { $mdDialog.cancel(); }; }; $scope.showDialog = function(){ $scope.draftDialogController.$inject = ['$scope', '$mdDialog', 'hotgenStates']; $mdDialog.show({ controller: $scope.draftDialogController, templateUrl: basePath + 'templates/modal_draft.html', parent: angular.element(document.body), clickOutsideToClose:true }).then(function(){ hotgenNotify.show_success('The draft is loaded successfully.'); }, function(){ // hotgenNotify.show_error('dismiss a modal'); }); } $scope.handle_load_draft = function(event, args){ $scope.showDialog(); } $scope.$on('handle_load_draft', $scope.handle_load_draft); }]); })(); ././@LongLink0000000000000000000000000000021700000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-template.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000001541013737340771034005 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.TempModalController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var hotgenGlobals, hotgenStates, hotgenNotify, basePath, projectPath; beforeEach(inject(function($injector){ hotgenGlobals = $injector.get('hotgenGlobals'); hotgenStates = $injector.get('hotgenStates'); hotgenNotify = $injector.get('hotgenNotify'); basePath = $injector.get('horizon.dashboard.project.heat_dashboard.template_generator.basePath'); projectPath = $injector.get('horizon.dashboard.project.heat_dashboard.template_generator.projectPath'); })); var $controller, controller, $scope, $rootScope, $mdDialog; beforeEach(inject(function($injector){ $rootScope = $injector.get('$rootScope'); $mdDialog = $injector.get('$mdDialog'); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); spyOn($mdDialog, 'show'); spyOn($mdDialog, 'cancel'); spyOn(hotgenNotify, 'show_success'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.TempModalController', { $scope: $scope, $mdDialog: $mdDialog}); $mdDialog.show = jasmine.createSpy().and.callFake(function() { return { then: (function (callBack) { callBack(true); //return the value to be assigned. }, function(callBack){ callBack(false)} ) } }); $mdDialog.cancel = jasmine.createSpy().and.callFake(function() { return function(callBack){callBack(true)}; }); })); it('should exist', function(){ expect(controller).toBeDefined(); expect($mdDialog).toBeDefined(); }); it('$scope.open', function(){ $scope.open(); expect($mdDialog.show).not.toHaveBeenCalled(); hotgenGlobals.set_template_version('2017-09-01.template'); $scope.open(); expect($mdDialog.show).toHaveBeenCalled(); }); it('$scope.dialogController', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); expect($scope.all_saved).toEqual(false); }); it('$scope.dialogController $cancel', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); $scope.cancel(); expect($mdDialog.cancel).toHaveBeenCalled(); }); it('$scope.dialogController download', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); $scope.download(); expect(hotgenNotify.show_success).toHaveBeenCalled(); }); it('$scope.dialogController save', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); $scope.redirect = jasmine.createSpy().and.callFake(function() { return function(){return true}; }); $scope.save(); expect($scope.redirect).toHaveBeenCalled(); }); it('$scope.dialogController extract_properties', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); var to_extract = { description: '', public_key: '', metadata: '', scheduler_hints: '', value_specs: '', allocation_pools: '', allowed_address_pairs: '', block_device_mapping: '', block_device_mapping_v2: '', fixed_ips: '', host_routes: '', personality: '', rules: '', dns_nameservers: '', dhcp_agent_ids: '', tags: '', resource_def: '', others: '' } var resource_data = $scope.extract_properties(to_extract); expect(Object.keys(resource_data).length).toEqual(2); }); it('$scope.dialogController generate', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); hotgenStates.update_saved_resources('node_id', { type: 'Node_Type', data: { description: '', public_key: '', metadata: '', scheduler_hints: '', value_specs: '', allocation_pools: '', allowed_address_pairs: '', block_device_mapping: '', block_device_mapping_v2: '', fixed_ips: '', host_routes: '', personality: '', rules: '', dns_nameservers: '', dhcp_agent_ids: '', tags: '', resource_def: '', others: ''} }); hotgenStates.set_saved_flags({'node_id': true}); hotgenStates.update_saved_dependsons('node_id', ['depend_on_id']); hotgenStates.set_incremented_label('depend_on_id', 'depend_on_label'); hotgenGlobals.set_resource_outputs('Node_Type', {'property': 'public_key'}); hotgenGlobals.set_template_version('2017-09-01.template'); var yamlstring = $scope.generate(); expect(yamlstring.length).toBeGreaterThan(0); }); it('$scope.dialogController trivial generate', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); hotgenStates.update_saved_resources('node_id', { type: 'Node_Type', data: {metadata: ''} }); hotgenStates.set_saved_flags({'node_id': true}); hotgenGlobals.set_template_version('2017-09-01.template'); var yamlstring = $scope.generate(); expect(yamlstring.length).toBeGreaterThan(0); }); it('$scope.dialogController trivial dependson generate', function(){ $scope.dialogController($scope, $mdDialog, hotgenStates, hotgenGlobals, hotgenNotify); hotgenStates.update_saved_resources('node_id', { type: 'Node_Type', data: {metadata: ''} }); hotgenStates.set_saved_flags({'node_id': true}); hotgenStates.update_saved_dependsons('node_id', []); hotgenGlobals.set_template_version('2017-09-01.template'); var yamlstring = $scope.generate(); expect(yamlstring.length).toBeGreaterThan(0); }); }); })(); ././@LongLink0000000000000000000000000000020200000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000612313737340771034006 0ustar zuulzuul00000000000000(function () { 'use strict'; describe('hotgen-agent module', function () { it('should be defined', function () { expect(angular.module('hotgen-agent')).toBeDefined(); }); }); describe('hotgen-utils.hotgenStates', function(){ beforeEach(module('hotgen-agent')); var hotgenAgent; beforeEach(inject(function(_hotgenAgent_){ hotgenAgent = _hotgenAgent_; })); var $httpBackend, requestHandler; var $location; beforeEach(inject(function($injector){ $location = $injector.get('$location'); })); beforeEach(inject(function($injector) { // Set up the mock http service responses $httpBackend = $injector.get('$httpBackend'); requestHandler = $httpBackend.when('GET', 'http://some-url/get_resource_options') .respond(200, { 'auth': { 'tenant_id': 'tenant-id', 'admin': false, }} ); })); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('should exist', function(){ expect(hotgenAgent).toBeDefined(); }); it('should return get_resource_options', function(){ spyOn($location, 'absUrl').and.callFake(function (p) { return 'http://some-url/'; }); $httpBackend.expectGET('http://some-url/get_resource_options'); var optionsPromise = hotgenAgent.get_resource_options(); optionsPromise.then(function(options){ expect(options.auth.tenant_id).toEqual('tenant-id'); expect(options.auth.admin).toEqual(false); }); $httpBackend.flush(); }); it('should return get_resource_options with errors', function(){ spyOn($location, 'absUrl').and.callFake(function (p) { return 'http://some-url/'; }); requestHandler.respond(200, { 'auth': { 'tenant_id': 'tenant-id', 'admin': false, },'errors': {'a': 'b'}} ); $httpBackend.expectGET('http://some-url/get_resource_options'); var optionsPromise = hotgenAgent.get_resource_options(); optionsPromise.then(function(options){ expect(options.auth.tenant_id).toEqual('tenant-id'); expect(options.auth.admin).toEqual(false); }); $httpBackend.flush(); }); it('should return error', function(){ spyOn($location, 'absUrl').and.callFake(function (p) { return 'http://some-url'; }); requestHandler.respond(500, ''); $httpBackend.expectGET('http://some-url/get_resource_options'); var optionsPromise = hotgenAgent.get_resource_options(); optionsPromise.then(function(options){ expect(options).toEqual(null); }); $httpBackend.flush(); }); }); })(); ././@LongLink0000000000000000000000000000020200000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/compile.directive.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000056213737340771034007 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('hotgen-utils') .directive('compile', [ '$compile', function($compile){ return { link: function(scope, element, attrs){ var content = $compile(attrs.compile)(scope); element.append(content); } } }]); })(); ././@LongLink0000000000000000000000000000020100000000000011206 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/icons.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000227413737340771034011 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.IconController', ['$scope', 'hotgenGlobals', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, hotgenGlobals, basePath){ $scope.resource_types = hotgenGlobals.get_resource_icons(); $scope.resource_admin = hotgenGlobals.get_node_admin(); $scope.admin = hotgenGlobals.get_resource_options().auth.admin; $scope.basePath = basePath; $scope.project_types = {}; for (var idx in $scope.resource_types){ var pidx = idx.split('__'); if (!pidx || pidx.length != 3){ continue } var level = pidx[0]+'::'+pidx[1] if (! (level in $scope.project_types)){ $scope.project_types[level] = {} } $scope.project_types[level][idx] = $scope.resource_types[idx]; } $scope.currentNavItem = Object.keys($scope.project_types)[0]; $scope.showIcon = function(){ // console.log($scope.currentNavItem) }; }]); })(); ././@LongLink0000000000000000000000000000021100000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/droppable.directive.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000436313737340771034012 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator droppable directive', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var hotgenGlobals; beforeEach(inject(function($injector){ hotgenGlobals = $injector.get('hotgenGlobals'); })); var $compile, $rootScope, $scope, $isolateScope, element; beforeEach(inject(function($rootScope, $compile) { $scope = $rootScope.$new(); // element will enable you to test your directive's element on the DOM element = $compile('
    drop me
    ')($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); // If the directive uses isolate scope, we need to get a reference to it // explicitly $isolateScope = element.isolateScope(); })); it('Replaces the element with the appropriate content', function() { expect(element.html()).toEqual('drop me'); }); it('Dragover event with preventDefault', function() { var mockEvent= { type: 'dragover', preventDefault: function(){}, } spyOn(mockEvent, 'preventDefault'); $scope.dragoverHandler(mockEvent); expect(mockEvent.preventDefault).toHaveBeenCalled(); }); it('Dragover event with !preventDefault', function() { var mockEvent= { type: 'dragover', } $scope.dragoverHandler(mockEvent); }); it('Drop event', function() { var resource_type = 'OS::Project::ResourceType'; hotgenGlobals.update_resource_icons(resource_type, { 'code': '', 'color': '#000' }) var mockEvent = { type: 'drop', dataTransfer: { getData: function(key){return resource_type}, }, target: {id: 'icon-1'}, preventDefault: function(){}, }; $scope.dropHandler(mockEvent); }); }); })(); ././@LongLink0000000000000000000000000000020300000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/loading.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000130313737340771034001 0ustar zuulzuul00000000000000(function(){ 'use strict'; angular .module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.LoadingController', [ '$scope', 'hotgenNotify', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, hotgenNotify, basePath){ $scope.loading = true; $scope.basePath = basePath; $scope.$on('handle_resources_loaded', function(event, args){ hotgenNotify.show_info('Close loading.'); $scope.loading = false; }); }]) })(); ././@LongLink0000000000000000000000000000020400000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/draggable.directive.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000132013737340771034000 0ustar zuulzuul00000000000000(function(){ 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator').directive('draggable', [function(){ return function ($scope, element){ var el = element[0]; el.draggable = true; $scope.dragstartHandler = function(e){ el.style.opacity = '0.4'; e.dataTransfer.setData('text', e.target.id); } $scope.dragendHandler = function(e){ el.style.opacity = '1.0'; } el.addEventListener('dragstart', $scope.dragstartHandler, false); el.addEventListener('dragend', $scope.dragendHandler, false); } }]); })();././@LongLink0000000000000000000000000000020700000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/compile.directive.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000160113737340771034002 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('hotgen-utils compile directive', function(){ beforeEach(module('hotgen-utils')); var $compile, $rootScope, $scope, $isolateScope, element; beforeEach(inject(function($rootScope, $compile) { $scope = $rootScope.$new(); // element will enable you to test your directive's element on the DOM element = $compile(angular.element('
    '))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); // If the directive uses isolate scope, we need to get a reference to it // explicitly })); it('Replaces the element with the appropriate content', function() { expect(element.html()).toContain("Compile Me"); }); }); })(); ././@LongLink0000000000000000000000000000021300000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-edge.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000001635513737340771034016 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.EdgeFormModalController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var hotgenGlobals, hotgenStates, basePath; beforeEach(inject(function($injector){ hotgenGlobals = $injector.get('hotgenGlobals'); hotgenStates = $injector.get('hotgenStates'); basePath = $injector.get('horizon.dashboard.project.heat_dashboard.template_generator.basePath'); })); var $controller, controller, $scope, $rootScope, $mdDialog; beforeEach(inject(function($injector){ $rootScope = $injector.get('$rootScope'); $mdDialog = $injector.get('$mdDialog'); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); spyOn($scope, '$on'); spyOn($mdDialog, 'show'); spyOn($mdDialog, 'cancel'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.EdgeFormModalController', { $scope: $scope, $mdDialog: $mdDialog}); $mdDialog.show = jasmine.createSpy().and.callFake(function() { return { then: (function (callBack) { callBack(true); //return the value to be assigned. }, function(callBack){ callBack(false)} ) } }); $mdDialog.cancel = jasmine.createSpy().and.callFake(function() { return function(callBack){callBack(true)}; }); })); it('should exist', function(){ expect(controller).toBeDefined(); }); it('$scope.$on should be called', function(){ var event = $rootScope.$broadcast('handle_edit_edge'); expect($scope.$on).toHaveBeenCalled(); }); it('show dialog with no parameters set.', function(){ $scope.showTabDialog(); $scope.$digest(); expect($mdDialog.show).toHaveBeenCalled(); }); it('handle_edit_edge valid edges', function(){ var event = $rootScope.$broadcast('handle_edit_edge'); hotgenGlobals.update_edge_directions('from_id', {'to_id': {}}); hotgenStates.update_saved_dependsons('from_id', 'to_id'); $scope.handle_edit_edge(event, { 'from_type': 'from_type', 'to_type': 'to_type', 'from_id': 'from_id', 'to_id': 'to_id', }); expect($mdDialog.show).toHaveBeenCalled(); }); it('handle_edit_edge invalid edges #1', function(){ var event = $rootScope.$broadcast('handle_edit_edge'); hotgenGlobals.update_edge_directions('from_type', {'to_type': {}}) expect($mdDialog.show).not.toHaveBeenCalled(); $scope.handle_edit_edge(event, { 'from_type': 'from_type', 'to_type': 'to_type', 'from_id': 'from_id', 'to_id': 'to_id', }); }); it('handle_edit_edge invalid edges #2', function(){ var event = $rootScope.$broadcast('handle_edit_edge'); hotgenGlobals.update_edge_directions('from_id', {'to': {}}) $scope.handle_edit_edge(event, { 'from_type': 'from_type', 'to_type': 'to_type', 'from_id': 'from_id', 'to_id': 'to_id', }); expect($mdDialog.show).not.toHaveBeenCalled(); }); it('cancel dialog', function(){ hotgenStates.set_selected({ id: 'edge-id', edge: {arrows: {middle: true}}, resource_type: {from: 'from_type', to: 'to_type'}, from_node: {id: 'from_id'}, to_node: {id: 'to_id'} }); hotgenStates.set_saved_resources({ 'edge-id': {data: {'resource property': 'something'}} }); $scope.edgeDialogController($scope, $mdDialog, hotgenStates, basePath); $scope.cancel(); expect($mdDialog.cancel).toHaveBeenCalled(); }); it('delete resource dialog', function(){ hotgenStates.set_selected({ id: 'edge-id', edge: {arrows: {middle: true}}, resource_type: {from: 'from_type', to: 'to_type'}, from_node: {id: 'from_id'}, to_node: {id: 'to_id'} }); hotgenStates.set_saved_resources({ 'edge-id': {data: {'resource property': 'something'}} }); hotgenStates.set_network({ deleteSelected: function(){} }); $scope.edgeDialogController($scope, $mdDialog, hotgenStates, basePath); $scope.delete_resource(); expect($mdDialog.cancel).toHaveBeenCalled(); }); it('handle_edit_edge test depends on edge.', function(){ hotgenStates.set_selected({ id: 'edge-id', edge: {arrows: {middle: true}}, resource_type: {from: 'from_type', to: 'to_type'}, from_node: {id: 'from_id'}, to_node: {id: 'to_id'} }); hotgenStates.set_saved_resources({ 'edge-id': {data: {'resource property': 'something'}} }); $scope.edgeDialogController($scope, $mdDialog, hotgenStates, basePath); expect($scope.is_depends).toEqual(true); expect(Object.keys($scope.resource).length).toEqual(1); expect($scope.from_node.image).toEqual(basePath+'js/resources/from_type/from_type.svg') }); it('handle_edit_edge test properties edge.', function(){ hotgenStates.set_selected({ id: 'edge-id', edge: {}, resource_type: {from: 'from_type', to: 'to_type'}, from_node: {id: 'from_id'}, to_node: {id: 'to_id'} }); hotgenStates.set_saved_resources({ 'edge-id': { data: {'resource property': 'something'} } }) $scope.edgeDialogController($scope, $mdDialog, hotgenStates, basePath); expect($scope.is_depends).toEqual(false); expect(Object.keys($scope.resource).length).toEqual(1); }); it('handle_edit_edge test not saved edge.', function (){ hotgenStates.set_selected({ id: 'edge-id', edge: {}, resource_type: {from: 'from_type', to: 'to_type'}, from_node: {id: 'from_id'}, to_node: {id: 'to_id'} }); hotgenStates.set_saved_resources({ 'node-id': { data: {'resource property': 'something'} } }) $scope.edgeDialogController($scope, $mdDialog, hotgenStates, basePath); expect(Object.keys($scope.resource).length).toEqual(0) }); }); })(); ././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-node.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002143213737340771034006 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.FormModalController', ['$scope', '$compile', '$mdDialog', 'hotgenNotify', 'hotgenMessage', 'hotgenGlobals', 'hotgenUtils', 'hotgenStates', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, $compile, $mdDialog, hotgenNotify, hotgenMessage, hotgenGlobals, hotgenUtils, hotgenStates, basePath){ $scope.selected = hotgenStates.get_selected(); $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), }; $scope.dialogController = function ($scope, $mdDialog, hotgenStates) { $scope.delete_resource = function() { var label = hotgenStates.get_selected().node.label; hotgenStates.get_network().deleteSelected(); hotgenNotify.show_success(label + ' has been delete successfully.') $mdDialog.cancel(); }; $scope.cancel = function() { $mdDialog.cancel(); }; $scope.selected = hotgenStates.get_selected(); $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), }; $scope.save = function() { $mdDialog.hide(); hotgenStates.update_saved_resources($scope.selected.id, { type: $scope.selected.resource_type, data: angular.copy($scope.resource) }); hotgenStates.update_saved_dependsons($scope.selected.id, $scope.dependson); var label = hotgenStates.get_label_by_uuid($scope.selected.node.id); var prop_label = $scope.get_label($scope.selected.resource_type); if (prop_label && $scope.resource[prop_label]){ label = label + '(' + $scope.resource[prop_label]+')'; } var new_node_image = $scope.data.nodes.get($scope.selected.id).image.replace('-gray.svg', '-blue.svg'); var shape = $scope.selected.node.shape; var color = "#3f51b5" if (shape === 'icon'){ color = $scope.selected.node.icon.color; } $scope.data.nodes.update({ id: $scope.selected.id, label: label, font: { color: color}, image: new_node_image, }) // Mark the node is saved. hotgenStates.update_saved_flags($scope.selected.id, true); // Update depends on edges var related_edges = hotgenStates.get_network().getConnectedEdges($scope.selected.id); for ( var idx in related_edges){ var edge = $scope.data.edges.get(related_edges[idx]); if (edge.from == $scope.selected.id && edge.arrows && edge.arrows.middle == true){ $scope.data.edges.remove(edge.id); } } for (var idx in $scope.dependson){ $scope.data.edges.add({ from: $scope.selected.id, to: $scope.dependson[idx], arrows: {middle:true}, dashes: false, color: '#448AFF', }); } // Mark edges connected from the node are saved and update style. for (var idx in $scope.connectedoptions){ var connected_option = $scope.connectedoptions[idx]; for (var idx_edge in connected_option){ hotgenStates.update_saved_flags(connected_option[idx_edge].edge.id, true); var color = "#3f51b5"; $scope.data.edges.update({ id: connected_option[idx_edge].edge.id, dashes: false, color: color, }) } } return true; }; $scope.resource_type = $scope.selected.resource_type.replace(/_/g, ':'); if ($scope.selected.id in hotgenStates.get_saved_resources()){ $scope.resource = hotgenStates.get_saved_resources()[$scope.selected.id].data; } else{ $scope.resource = {} } if ($scope.selected.id in hotgenStates.get_saved_dependsons()){ $scope.dependson = hotgenStates.get_saved_dependsons()[$scope.selected.id]; } else{ $scope.dependson = [] } // Add connected edge resource $scope.get_connected_options = function(){ var related_edges = hotgenStates.get_network().getConnectedEdges($scope.selected.id); var connected_options = {}; for (var idx in related_edges){ var edge = $scope.data.edges.get(related_edges[idx]); if (edge.from != $scope.selected.id || (edge.arrows && edge.arrows.middle == true)){ continue; } var node = $scope.data.nodes.get(edge.to); var edge_directions = hotgenGlobals.get_edge_directions(); if (! ($scope.selected.resource_type in edge_directions)){ continue; } var mapping = edge_directions[$scope.selected.resource_type]; if (!(node.title in mapping)){ continue; } var property = mapping[node.title].property; if (!(property in connected_options)){ connected_options[property] = []; } connected_options[property].push({ value: hotgenUtils.get_resource_string(hotgenStates.get_label_by_uuid(node.id)), id: node.id, resource_type: node.title, edge: edge }); } return connected_options; } $scope.connectedoptions = $scope.get_connected_options(); $scope.component = hotgenGlobals.get_resource_components()[$scope.selected.resource_type]; $scope.get_label = function(node_type){ return hotgenGlobals.get_node_labels()[node_type]; } } $scope.showTabDialog = function(){ $scope.dialogController.$inject = ['$scope', '$mdDialog', 'hotgenStates']; $mdDialog.show({ controller: $scope.dialogController, controllerAs: 'ctrl', templateUrl: basePath+'templates/modal_resource.html', parent: angular.element(document.body), clickOutsideToClose:true }).then(function(){ hotgenNotify.show_success('The selected resource is saved successfully.'); }, function(){ // hotgenNotify.show_error('dismiss a modal'); }); }; $scope.handle_edit_node = function(event, args){ hotgenNotify.show_info('Show details of resource ' + args.replace(/_/g, ':') +'.'); $scope.showTabDialog(); } $scope.$on('handle_edit_node', $scope.handle_edit_node); }]); })(); ././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-edge.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000001027713737340771034013 0ustar zuulzuul00000000000000(function(){ 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.EdgeFormModalController', ['$scope', '$mdDialog', 'hotgenNotify', 'hotgenMessage', 'hotgenGlobals', 'hotgenStates', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, $mdDialog, hotgenNotify, hotgenMessage, hotgenGlobals, hotgenStates, basePath){ $scope.edgeDialogController = function($scope, $mdDialog, hotgenStates, basePath) { $scope.is_depends = false; $scope.delete_resource = function() { hotgenStates.get_network().deleteSelected(); hotgenNotify.show_success('The selected edge has been delete successfully.') $mdDialog.cancel(); }; $scope.cancel = function() { $mdDialog.cancel(); }; $scope.selected = hotgenStates.get_selected(); if ($scope.selected.id in hotgenStates.get_saved_resources()){ $scope.resource = hotgenStates.get_saved_resources()[$scope.selected.id].data; } else{ $scope.resource = {} } if ($scope.selected.edge.arrows && $scope.selected.edge.arrows.middle == true){ $scope.is_depends = true; } var from_type = $scope.selected.resource_type.from; var to_type = $scope.selected.resource_type.to; $scope.from_type = from_type.replace(/_/g, ':'); $scope.to_type = to_type.replace(/_/g, ':'); $scope.from_node = { image: basePath+'js/resources/'+from_type.toLowerCase()+'/'+from_type.toLowerCase()+'.svg', id: $scope.selected.from_node.id, } $scope.to_node = { image: basePath+'js/resources/'+to_type.toLowerCase()+'/'+to_type.toLowerCase()+'.svg', id: $scope.selected.to_node.id, } } $scope.showTabDialog = function(){ $scope.edgeDialogController.$inject = ['$scope', '$mdDialog', 'hotgenStates', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath']; $mdDialog.show({ controller: $scope.edgeDialogController, controllerAs: 'ctrl', templateUrl: basePath+'templates/modal_edge.html', parent: angular.element(document.body), clickOutsideToClose:true }).then(function(){ // hotgenNotify.show_success('close the modal'); }, function(){ // hotgenNotify.show_error('dismiss a modal'); }); }; $scope.handle_edit_edge = function(event, args){ /* Click a edge and decide to show modal or not */ var from_type = args.from_type; var to_type = args.to_type; var from_id = args.from_id; var to_id = args.to_id; var edge_directions = hotgenGlobals.get_edge_directions(); var depends_ons = hotgenStates.get_saved_dependsons(); if ( !( from_type in edge_directions) || !(to_type in edge_directions[from_type])){ if (from_id in depends_ons && depends_ons[from_id] == to_id){ //; } else { hotgenNotify.show_warning('The edge might be invalid.'); return; } } $scope.showTabDialog(); }; $scope.$on('handle_edit_edge', $scope.handle_edit_edge); }]); })(); ././@LongLink0000000000000000000000000000021700000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/template-generator.module.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000150413737340771034004 0ustar zuulzuul00000000000000(function () { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator module', function () { it('should be defined', function () { expect(angular.module('horizon.dashboard.project.heat_dashboard.template_generator')).toBeDefined(); }); }); describe('horizon.dashboard.project.heat_dashboard.template_generator.basePath', function () { beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); it('should be defined and set correctly', inject([ 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', '$window', function (basePath, $window) { expect(basePath).toBeDefined(); expect(basePath).toBe($window.STATIC_URL + 'dashboard/project/heat_dashboard/template_generator/'); }]) ); }); })(); ././@LongLink0000000000000000000000000000021300000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-node.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002726313737340771034016 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.FormModalController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); var hotgenGlobals, hotgenStates, basePath; beforeEach(inject(function($injector){ hotgenGlobals = $injector.get('hotgenGlobals'); hotgenStates = $injector.get('hotgenStates'); basePath = $injector.get('horizon.dashboard.project.heat_dashboard.template_generator.basePath'); })); var $controller, controller, $scope, $rootScope, $mdDialog; beforeEach(inject(function($injector){ $rootScope = $injector.get('$rootScope'); $mdDialog = $injector.get('$mdDialog'); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); spyOn($scope, '$on'); spyOn($mdDialog, 'show'); spyOn($mdDialog, 'hide'); spyOn($mdDialog, 'cancel'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.FormModalController', { $scope: $scope, $mdDialog: $mdDialog}); $mdDialog.show = jasmine.createSpy().and.callFake(function() { return { then: (function (callBack) { callBack(true); //return the value to be assigned. }, function(callBack){ callBack(false)} ) } }); $mdDialog.cancel = jasmine.createSpy().and.callFake(function() { return function(callBack){callBack(true)}; }); })); it('should exist', function(){ expect(controller).toBeDefined(); }); it('$scope.$on should be called', function(){ var event = $rootScope.$broadcast('handle_edit_node'); expect($scope.$on).toHaveBeenCalled(); }); it('$scope.handle_edit_node', function(){ var event = $rootScope.$broadcast('handle_edit_node'); spyOn($scope, 'showTabDialog'); $scope.handle_edit_node(event, 'OS__Project__ResourceType'); expect($scope.showTabDialog).toHaveBeenCalled(); }); it('show dialog with no parameters set.', function(){ $scope.showTabDialog(); $scope.$digest(); expect($mdDialog.show).toHaveBeenCalled(); }); it('cancel dialog', function(){ var node = {id: 'node_id', label: 'Node_1', title: 'NodeType_1'} var to_node = {id: 'to_node_id', label: 'ToNode_1', title: 'ToType_1'} var edge1 = {id: 'edge_id_1', from: 'node_id', to: 'to_node_id'} var edge2 = {id: 'edge_id_2', from: 'from_node_id', to: 'node_id'} hotgenStates.set_selected({ id: node.id, node: node, resource_type: node.title, }); var network = {getConnectedEdges: function(data){ return [edge1.id, edge2.id]; }} hotgenStates.set_network(network); $scope.data.nodes.add(node); $scope.data.edges.add(edge1); $scope.data.edges.add(edge2); $scope.dialogController($scope, $mdDialog, hotgenStates); $scope.cancel(); expect($mdDialog.cancel).toHaveBeenCalled(); }); it('get label', function(){ var node = {id: 'node_id', label: 'Node_1', title: 'NodeType_1'} var to_node = {id: 'to_node_id', label: 'ToNode_1', title: 'ToType_1'} var edge1 = {id: 'edge_id_1', from: 'node_id', to: 'to_node_id'} var edge2 = {id: 'edge_id_2', from: 'from_node_id', to: 'node_id'} hotgenStates.set_selected({ id: node.id, node: node, resource_type: node.title, }); var network = {getConnectedEdges: function(data){ return [edge1.id, edge2.id]; }} hotgenStates.set_network(network); $scope.data.nodes.add(node); $scope.data.edges.add(edge1); $scope.data.edges.add(edge2); hotgenGlobals.update_node_labels(node.title, node.label) $scope.dialogController($scope, $mdDialog, hotgenStates); var nodelabel = $scope.get_label('NodeType_1'); expect(nodelabel).toEqual(node.label); }); it('delete resource', function(){ var node = {id: 'node_id', label: 'Node_1', title: 'NodeType_1'} var to_node = {id: 'to_node_id', label: 'ToNode_1', title: 'ToType_1'} var edge1 = {id: 'edge_id_1', from: 'node_id', to: 'to_node_id'} var edge2 = {id: 'edge_id_2', from: 'from_node_id', to: 'node_id'} hotgenStates.set_selected({ id: node.id, node: node, resource_type: node.title, }); var network = { deleteSelected: function(){ return true; }, getConnectedEdges: function(data){ return [edge1.id, edge2.id]; } }; hotgenStates.set_network(network); $scope.data.nodes.add(node); $scope.data.edges.add(edge1); $scope.data.edges.add(edge2); $scope.dialogController($scope, $mdDialog, hotgenStates); $scope.delete_resource(); expect($mdDialog.cancel).toHaveBeenCalled(); }); it('save without depends_on', function(){ var node = {id: 'node_id', label: 'Node_1', title: 'NodeType_1', image: 'image-gray.svg'} hotgenStates.set_selected({ id: 'node_id', node: node, resource_type: 'NodeType_1', }); var network = { deleteSelected: function(){ return true; }, getConnectedEdges: function(data){ return []; } }; $scope.data.nodes.add(node); hotgenGlobals.update_node_labels(node.id, node.label); hotgenStates.set_incremented_label(node.id, node.label); hotgenStates.set_network(network); $scope.dialogController($scope, $mdDialog, hotgenStates); $scope.dependson = []; var returnValue = $scope.save(); expect($mdDialog.hide).toHaveBeenCalled(); expect(returnValue).toEqual(true); }); it('save with depends_on', function(){ var node = {id: 'node_id', label: 'Node_1', title: 'NodeType_1', icon: {color: '#000'}, shape: 'icon', image: 'image-gray.svg'} var depend_node = {id: 'depend_node_id', label: 'Node_2', title: 'NodeType_2'} var connected_node = {id: 'conn_node_id', label: 'Node_3', title: 'NodeType_3'} var connected_node2 = {id: 'conn_node2_id', label: 'Node_4', title: 'NodeType_3'} var edge = { id: 'edge_id', from: node.id, to: connected_node.id, resource_type: {from: node.title, to: connected_node.title}, from_node: node, to_node: connected_node}; var edge2 = { id: 'edge2_id', from: node.id, to: connected_node2.id, resource_type: {from: node.title, to: connected_node2.title}, from_node: node, to_node: connected_node2}; var depend_edge = {id: 'depend_edge_id', from: node.id, to: depend_node.id, resource_type: {from: node.title, to: depend_node.title}, from_node: node, to_node: depend_node, arrows: {middle: true}}; $scope.data.nodes.add(node); $scope.data.nodes.add(depend_node); $scope.data.nodes.add(connected_node); $scope.data.nodes.add(connected_node2); $scope.data.edges.add(edge); $scope.data.edges.add(edge2); $scope.data.edges.add(depend_edge); hotgenGlobals.update_node_labels(node.title, 'name'); hotgenGlobals.update_edge_directions(node.title, {'NodeType_3': {property: 'name'}}); hotgenStates.update_saved_resources(node.id, {type: node.title, data: {name: node.label}}) hotgenStates.set_incremented_label(node.id, node.label); hotgenStates.update_saved_dependsons(node.title, [depend_node.id]); hotgenStates.update_saved_dependsons(node.id, [depend_node.id]); hotgenStates.set_selected({ id: 'node_id', node: node, resource_type: 'NodeType_1', }); var network = { deleteSelected: function(){ return true; }, getConnectedEdges: function(data){ return [edge.id, edge2.id, depend_edge.id,]; } }; hotgenStates.set_network(network); $scope.dialogController($scope, $mdDialog, hotgenStates); $scope.dependson = [depend_node.id]; var returnValue = $scope.save(); expect($mdDialog.hide).toHaveBeenCalled(); expect(returnValue).toEqual(true); }); it('save with trivial cases', function(){ var node = {id: 'node_id', label: 'Node_1', title: 'NodeType_1', icon: {color: '#000'}, shape: 'icon', image: 'image-gray.svg'} var depend_node = {id: 'depend_node_id', label: 'Node_2', title: 'NodeType_2'} var connected_node = {id: 'conn_node_id', label: 'Node_3', title: 'NodeType_3'} var edge = { id: 'edge_id', from: node.id, to: connected_node.id, resource_type: {from: node.title, to: connected_node.title}, from_node: node, to_node: connected_node}; var depend_edge = {id: 'depend_edge_id', from: node.id, to: depend_node.id, resource_type: {from: node.title, to: depend_node.title}, from_node: node, to_node: depend_node, arrows: {middle: true}}; $scope.data.nodes.add(node); $scope.data.nodes.add(depend_node); $scope.data.nodes.add(connected_node); $scope.data.edges.add(edge); $scope.data.edges.add(depend_edge); hotgenGlobals.update_node_labels(node.title, 'name'); hotgenGlobals.update_edge_directions(node.title, {'NodeType_2': {}}); hotgenStates.update_saved_resources(node.id, {type: node.title, data: {name: node.label}}) hotgenStates.set_incremented_label(node.id, node.label); hotgenStates.update_saved_dependsons(node.title, [depend_node.id]); hotgenStates.update_saved_dependsons(node.id, [depend_node.id]); hotgenStates.set_selected({ id: node.id, node: node, resource_type: node.title, }); var network = { deleteSelected: function(){ return true; }, getConnectedEdges: function(data){ return [edge.id, depend_edge.id]; } }; hotgenStates.set_network(network); $scope.dialogController($scope, $mdDialog, hotgenStates); $scope.dependson = [depend_node.id]; var returnValue = $scope.save(); expect($mdDialog.hide).toHaveBeenCalled(); expect(returnValue).toEqual(true); }); }); })(); ././@LongLink0000000000000000000000000000017500000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/utils.module.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002252713737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('hotgen-utils', ['cgNotify', 'angular-uuid', 'ngVis']) .factory('hotgenUUID', ['uuid', function(uuid) { return { uuid: uuid.v4, }; }]) .factory('hotgenNotify', ['notify', '$rootScope', function(notify, $rootScope) { notify.config({ position: 'right', duration: 0, }); var show_success = function(message) { if ($rootScope.message_level < 2){ return -1; } notify({ message: message, classes: ['alert-success',], duration: 3000, }); return 0; }; var show_error = function(message) { if ($rootScope.message_level < 0){ return -1; } notify({ message: message, classes: ['alert-danger',], // duration: 5000, }); return 0; }; var show_info = function(message) { if ($rootScope.message_level < 3){ return -1; } notify({ message: message, classes: ['alert-info',], duration: 3000, }); return 0; }; var show_warning = function(message) { if ($rootScope.message_level < 1){ return -1; } notify({ message: message, classes: ['alert-warning',], duration: 5000, }); return 0; }; return { show_success: show_success, show_error: show_error, show_info: show_info, show_warning: show_warning }; }]) .factory('hotgenMessage', ['$rootScope', function($rootScope){ var broadcast_edit_node = function(node_type){ $rootScope.$broadcast('handle_edit_node', node_type); }; var broadcast_edit_edge = function(from_type, to_type, from_id, to_id){ $rootScope.$broadcast('handle_edit_edge',{ 'from_type': from_type, 'to_type': to_type, 'from_id': from_id, 'to_id': to_id, }); }; var broadcast_load_draft = function(){ $rootScope.$broadcast('handle_load_draft'); }; var broadcast_resources_loaded = function(){ $rootScope.$broadcast('handle_resources_loaded'); }; var broadcast_update_template_version = function(){ $rootScope.$broadcast('update_template_version'); }; return { broadcast_edit_node: broadcast_edit_node, broadcast_edit_edge: broadcast_edit_edge, broadcast_load_draft: broadcast_load_draft, broadcast_resources_loaded: broadcast_resources_loaded, broadcast_update_template_version: broadcast_update_template_version, } }]) .factory('hotgenUtils', function(){ var get_resource_string = function(identity){ return '{ get_resource: '+identity+' }'; } var filter_and_return_get_resource_element = function(array, property){ var return_val = []; var idx = array.length-1; while (idx >= 0) { if (typeof array[idx] == 'string'){ if (array[idx].indexOf('get_resource') != -1){ return_val = return_val.concat(array.splice(idx, 1)) } } else if (typeof array[idx] == 'object' && property){ if (array[idx][property] && array[idx][property].indexOf('get_resource') != -1){ return_val = return_val.concat(array.splice(idx, 1)) } } idx = idx-1; } return return_val; } var escape_characters = function(value){ return '"'+value.replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, "\\n")+'"'; } var extract_keyvalue = function(value){ var new_keyvalue = {} if (value instanceof Array ){ for (var idx in value){ if (value[idx] instanceof Object ){ if (Object.keys(value[idx]).length == 0){ continue; } new_keyvalue[value[idx].key] = value[idx].value } } } if (Object.keys(new_keyvalue).length == 0){ return null; } return new_keyvalue; } var extract_list_of_keyvalue = function(value_list){ if (value_list instanceof Array ){ for (var idx=value_list.length-1; idx>=0; --idx){ if (Object.keys(value_list[idx]).length == 0){ value_list.splice(idx, 1) } } if (value_list.length == 0){ return null; } return value_list } return null; } var extract_list = function(value_list){ if (value_list instanceof Array){ if (value_list.length == 0){ return null; } return value_list } return null; } var extract_dicts = function check_dicts(value_dict){ for (var key in value_dict){ if (! value_dict[key]){ delete value_dict[key]; } } return value_dict } var extract_dict = function extract_dict(value_dict){ for (var key in value_dict){ strip_property(value_dict, key) } } var extract_array = function extract_array(value_list){ for (var idx=value_list.length-1; idx>=0; --idx){ if (value_list[idx] == null || value_list[idx] == ''){ value_list.splice(idx, 1) } else if (value_list[idx].constructor && value_list[idx].constructor == Object){ extract_dict(value_list[idx]) if (Object.keys(value_list[idx]).length == 0){ value_list.splice(idx, 1); } } else if (value_list[idx].constructor && value_list[idx].constructor == Array){ extract_array(value_list[idx]) if (value_list[idx].length == 0){ value_list.splice(idx, 1); } } } } var extract_resource_def = function extract_resource_def(value_dict){ if (value_dict.properties){ value_dict.properties = extract_keyvalue(value_dict.properties) } if (value_dict.metadata){ value_dict.metadata = extract_keyvalue(value_dict.metadata) } return value_dict } var strip_property = function check_property(resource_data, property){ if (resource_data[property] == null || resource_data[property] == ''){ delete resource_data[property]; } else if (resource_data[property].constructor && resource_data[property].constructor == Object){ extract_dict(resource_data[property]); if (Object.keys(resource_data[property]).length == 0){ delete resource_data[property]; } } else if (resource_data[property].constructor && resource_data[property].constructor == Array){ extract_array(resource_data[property]); if (resource_data[property].length == 0){ delete resource_data[property]; } } } return { get_resource_string: get_resource_string, escape_characters: escape_characters, extract_keyvalue: extract_keyvalue, extract_list_of_keyvalue: extract_list_of_keyvalue, extract_list: extract_list, extract_dicts: extract_dicts, extract_resource_def: extract_resource_def, filter_and_return_get_resource_element: filter_and_return_get_resource_element, strip_property: strip_property, }; }) })(); ././@LongLink0000000000000000000000000000017500000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000346013737340771034007 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('hotgen-agent', ['hotgen-utils', ]) .factory('hotgenAgent', ['$http', '$location', 'hotgenNotify', function($http, $location, hotgenNotify) { var get_resource_options = function(){ var static_url = $location.absUrl(); if (static_url.substr(-1) != '/'){ static_url += '/'; } return $http({ method: 'GET', url: static_url+'get_resource_options' }).then(function successCallback(response) { // this callback will be called asynchronously // when the response is available if (response.data.errors){ var msg = ''; angular.forEach(response.data.errors, function(value, key){ msg += key + ': '+ value + '. ' }) hotgenNotify.show_warning('Unable to retrieve resources '+msg+'.'); } else{ hotgenNotify.show_success('Retrieve openstack resources successfully.'); } return response.data; }, function errorCallback(response) { // called asynchronously if an error occurs // or server returns response with an error status. hotgenNotify.show_error('Cannot get openstack resources.'); return null; }); } return { get_resource_options: get_resource_options, }; }]) })(); ././@LongLink0000000000000000000000000000021200000000000011210 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/template-generator.module.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000355013737340771034007 0ustar zuulzuul00000000000000(function() { 'use strict'; angular .module('horizon.dashboard.project.heat_dashboard.template_generator', ['ngMaterial', 'ngMessages', 'ngSanitize', 'hotgen-utils', 'hotgen-agent', 'ui.bootstrap', ]) .config(['$mdThemingProvider', function($mdThemingProvider) { $mdThemingProvider.theme('default') .primaryPalette('indigo') .accentPalette('blue') .warnPalette('red') ; }]) .config(['$provide', '$windowProvider', function($provide, $windowProvider){ var project_window = $windowProvider.$get(); var staticPath = project_window.STATIC_URL + 'dashboard/project/heat_dashboard/template_generator/'; var projectPath = project_window.WEBROOT+'project/'; $provide.constant('horizon.dashboard.project.heat_dashboard.template_generator.basePath', staticPath); $provide.constant('horizon.dashboard.project.heat_dashboard.template_generator.projectPath', projectPath); }]) .constant('horizon.dashboard.project.heat_dashboard.template_generator.validationRules', { 'name': /^[A-Za-z0-9_.-]+$/, 'path': /^\/[a-z0-9/-]+$/, 'integer': /^\d*$/, 'keypair': /^([A-Za-z0-9_.-]{1,255})=([A-Za-z0-9_.-]{1,255})$/, 'ip_address': /^([0-9.]{1,15})|([A-Fa-f0-9:]{1,39})$/, 'domain': /^[A-Za-z0-9_.-]+$/, 'uuid4': /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/, 'uuid_nohyphen': /^[a-f0-9]{32}$/, 'mac_address': /^([A-Fa-f0-9]{2}[:-]){5}([A-Fa-f0-9]{2})$/, 'cidr': /^(([0-9.]{1,15})(\/([0-9]|[1-2][0-9]|3[0-2]))?)|(([A-Fa-f0-9:]{1,39})(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?)$/, 'zone': /^[a-zA-Z0-9_.-]{1,127}\.$/, }) ; })(); ././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000742213737340771034011 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $httpBackend, requestHandler; var $location; var hotgenGlobals; beforeEach(inject(function($injector){ $location = $injector.get('$location'); hotgenGlobals = $injector.get('hotgenGlobals'); spyOn($location, 'absUrl').and.callFake(function (p) { return 'http://some-url/'; }); })); beforeEach(inject(function($injector) { // Set up the mock http service responses $httpBackend = $injector.get('$httpBackend'); requestHandler = $httpBackend.when('GET', 'http://some-url/get_resource_options') .respond(200, { 'auth': { 'tenant_id': 'tenant-id', 'admin': false, }, 'template_versions': [ {'name': 'v1', 'id': 'v1'}, {'name': 'v2', 'id': 'v2'} ], } ); })); var $controller, controller, $scope; beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); })); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('should exist', function(){ controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', { $scope: $scope,}); expect(controller).toBeDefined(); $httpBackend.flush(); }); it('should return array with 2 items', function(){ $httpBackend.expectGET('http://some-url/get_resource_options'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', { $scope: $scope,}); $httpBackend.flush(); expect($scope.template_versions.length).toEqual(2); }); it('should return empty array', function(){ requestHandler.respond(500, ''); $httpBackend.expectGET('http://some-url/get_resource_options'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', { $scope: $scope,}); $httpBackend.flush(); expect($scope.template_versions.length).toEqual(0); }); it('should return true', function(){ $httpBackend.expectGET('http://some-url/get_resource_options'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', { $scope: $scope,}); $httpBackend.flush(); expect($scope.update_template_version()).toEqual(true); }); it('should set template version from hotgenGlobals', function(){ $httpBackend.expectGET('http://some-url/get_resource_options'); hotgenGlobals.set_template_version('template_version-1'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.AgentController', { $scope: $scope,}); $httpBackend.flush(); $scope.load_template_version(); expect($scope.template_version).toEqual('template_version-1'); }); }); })(); ././@LongLink0000000000000000000000000000021100000000000011207 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/draggable.directive.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000327613737340771034014 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator draggable directive', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); var $compile, $rootScope, $scope, $isolateScope, element; beforeEach(inject(function($rootScope, $compile) { $scope = $rootScope.$new(); // element will enable you to test your directive's element on the DOM element = $compile(angular.element('
    drag me
    '))($scope); // Digest needs to be called to set any values on the directive's scope $scope.$digest(); // If the directive uses isolate scope, we need to get a reference to it // explicitly $isolateScope = element.isolateScope(); })); it('Replaces the element with the appropriate content', function() { expect(element[0].draggable).toEqual(true); }); it('should change style when drag start', function() { var mockEvent = { 'type': 'dragstart', 'dataTransfer': { 'setData': function(key, value){}, }, 'target': {'id': 'icon-1'} }; $scope.dragstartHandler(mockEvent, element); expect(element[0].style.opacity).toEqual('0.4') }); it('should change style when drag end', function() { var mockEvent = { 'type': 'dragend', }; $scope.dragendHandler(mockEvent, element); expect(element[0].style.opacity).toEqual('1') }); }); })(); ././@LongLink0000000000000000000000000000021400000000000011212 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/modal-draft.controller.spec.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000562613737340771034015 0ustar zuulzuul00000000000000(function() { 'use strict'; describe('horizon.dashboard.project.heat_dashboard.template_generator.DraftModalController', function(){ beforeEach(module('horizon.dashboard.project.heat_dashboard.template_generator')); beforeEach(module('appTemplates')); beforeEach(module('hotgen-utils')); var hotgenStates; beforeEach(inject(function(_hotgenStates_){ hotgenStates = _hotgenStates_; })); var $controller, controller, $scope, $rootScope, $mdDialog; beforeEach(inject(function($injector){ $rootScope = $injector.get('$rootScope'); $mdDialog = $injector.get('$mdDialog'); })); beforeEach(inject(function(_$controller_, $rootScope) { $controller = _$controller_; $scope = $rootScope.$new(); spyOn($scope, '$on'); spyOn($mdDialog, 'show'); controller = $controller('horizon.dashboard.project.heat_dashboard.template_generator.DraftModalController', { $scope: $scope, $mdDialog: $mdDialog}); $mdDialog.show = jasmine.createSpy().and.callFake(function() { return { then: (function (callBack) { callBack(true); //return the value to be assigned. }, function(callBack){ callBack(false)} ) } }); })); afterEach(function(){ localStorage.clear(); $mdDialog.cancel(); }) it('should exist', function(){ expect(controller).toBeDefined(); }); it('$scope.$on should be called', function(){ var event = $rootScope.$broadcast('handle_load_draft'); expect($scope.$on).toHaveBeenCalled(); $scope.handle_load_draft(event, {}); }); it('show dialog', function(){ $scope.showDialog(); $scope.$digest(); expect($mdDialog.show).toHaveBeenCalled(); }); it('draftDialogController', function(){ var draft = {} localStorage.setItem('draft_1', JSON.stringify(draft)) $scope.draftDialogController($scope, $mdDialog, hotgenStates); expect($scope.data.nodes.length).toEqual(0); }); it('draftDialogController load draft', function(){ var draft = {'nodes': {'node-1':{}}, 'edges': {'edge-1':{}}}; $scope.draftDialogController($scope, $mdDialog, hotgenStates); $scope.load(draft); expect($scope.data.nodes.length).toEqual(1); expect($scope.data.edges.length).toEqual(1); }); it('draftDialogController cancel', function(){ $scope.draftDialogController($scope, $mdDialog, hotgenStates); $scope.cancel(); }); }); })(); ././@LongLink0000000000000000000000000000020700000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/vis-network.controller.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000002451713737340771034015 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator') .controller('horizon.dashboard.project.heat_dashboard.template_generator.VisController', ['$scope', '$rootScope', 'hotgenNotify', 'hotgenMessage', 'hotgenGlobals', 'hotgenStates', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function($scope, $rootScope, hotgenNotify, hotgenMessage, hotgenGlobals, hotgenStates, basePath) { $rootScope.message_level = 2; hotgenStates.clear_states(); $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), }; $scope.options = { autoResize: true, edges: { smooth: false, // arrows: 'to', dashes: true, // length: 300, color: { color: '#848484', highlight: '#848484', hover: '#848484', } }, nodes: { brokenImage: basePath+'css/img/icons/unknown-gray.svg', }, interaction: { hover: true, }, physics: { enabled: true, solver: 'forceAtlas2Based', forceAtlas2Based: { avoidOverlap: 1, }, barnesHut: { avoidOverlap: 1 } }, manipulation: { enabled: true, addEdge: function(data, callback){ var valid = $scope.validate_edge(data); if (valid == false){ callback(null); } else{ hotgenNotify.show_success("Successfully connected."); callback(data); $scope.data.nodes.update({ id: data.from, font: {color: '#343434'} }) hotgenStates.update_saved_flags(data.id, false); hotgenStates.update_saved_flags(data.from, false); } }, addNode: false, editEdge: false, deleteNode: function(data, callback){ var node_ids = data.nodes; var edge_ids = data.edges; for (var idx in node_ids){ delete hotgenStates.get_saved_flags()[node_ids[idx]]; hotgenStates.delete_saved_resources(node_ids[idx]); } for (var idx in edge_ids){ delete hotgenStates.get_saved_flags()[edge_ids[idx]]; hotgenStates.delete_saved_resources(edge_ids[idx]); } callback(data); }, deleteEdge: function(data, callback){ var edge_ids = data.edges; for (var idx in edge_ids){ var edge_id = edge_ids[idx]; delete hotgenStates.get_saved_flags()[edge_id]; var from_id = $scope.data.edges.get(edge_id).from; hotgenStates.update_saved_flags(from_id, false); $scope.data.nodes.update({ id: from_id, font: {color: '#343434'} }) } callback(data); }, }, }; $scope.click = function(params){ if (params.nodes.length > 0){ $scope.network.disableEditMode(); var selected_id = params.nodes[0]; var selected_node = $scope.data.nodes.get(selected_id); var selected_type = selected_node.title hotgenStates.set_selected({ element: 'node', resource_type: selected_type, id: selected_id, node: selected_node, }) ; hotgenMessage.broadcast_edit_node(selected_type); } else if (params.edges.length > 0){ $scope.network.disableEditMode(); var selected_id = params.edges[0]; var selected_edge = $scope.data.edges.get(selected_id); var from_node = $scope.data.nodes.get(selected_edge.from); var to_node = $scope.data.nodes.get(selected_edge.to); hotgenStates.set_selected({ element: 'edge', resource_type: {from: from_node.title, to: to_node.title}, from_node: from_node, to_node: to_node, id: selected_id, edge: selected_edge, }); hotgenMessage.broadcast_edit_edge(from_node.title, to_node.title, from_node.id, to_node.id); } else { // ; } }; $scope.events = { click: $scope.click, onload: function(network){ $scope.network = network; hotgenStates.set_network(network); } }; $scope.get_added_edge_id = function(old_edge_ids, new_edge_ids){ for (var id in new_edge_ids){ if (old_edge_ids.indexOf(new_edge_ids[id]) == -1){ return new_edge_ids[id]; } } } $scope.get_modal = function(data){ var from_node = $scope.get_node(data.from); var to_node = $scope.get_node(data.to); var mapping = $scope.get_mapping(from_node.title, to_node.title); if (mapping){ return mapping.modal; } return ; } $scope.get_mapping = function (from_type, to_type){ var edge_directions = hotgenGlobals.get_edge_directions() if ((! edge_directions[from_type]) || !(edge_directions[from_type][to_type])){ hotgenNotify.show_error(to_type.replace(/_/g, ':')+" cannot be connected with "+from_type.replace(/_/g, ':')+"."); return false; } return edge_directions[from_type][to_type] } $scope.get_node = function(node_id){ return $scope.data.nodes.get(node_id); } $scope.validate_edge = function(data){ if (data.from == data.to ){ // hotgenNotify.show_warning(""); return false; } var from_node = $scope.get_node(data.from); var to_node = $scope.get_node(data.to); var from_node_type = from_node.title; var to_node_type = to_node.title; var edge_directions = hotgenGlobals.get_edge_directions() if ((! edge_directions[from_node_type]) || !(edge_directions[from_node_type][to_node_type])){ hotgenNotify.show_error(to_node.label+" cannot be connected with "+from_node.label+"."); return false; } else{ var limit = edge_directions[from_node_type][to_node_type].limit; var occupied = edge_directions[from_node_type][to_node_type].occupied; var lonely = edge_directions[from_node_type][to_node_type].lonely; var from_connected = $scope.network.getConnectedNodes(data.from); var count = 0; for(var idx in from_connected){ var item_title = $scope.data.nodes.get(from_connected[idx]).title; if (to_node_type == item_title){ count += 1; } } if (count >= limit){ hotgenNotify.show_error("The number of connections between the resources is out of limit."); return false; } var to_connected = $scope.network.getConnectedNodes(data.to); if (lonely === true && to_connected.length > 0) { hotgenNotify.show_error(to_node.label+" cannot be connected with "+from_node.label+"."); return false; } if (occupied === true){ for (var idx in to_connected){ var item_title = $scope.data.nodes.get(to_connected[idx]).title; if (from_node_type == item_title){ hotgenNotify.show_error(to_node.label+" has already been connected with "+from_node_type+"."); return false; } } } } return true; } } ]); })(); ././@LongLink0000000000000000000000000000020400000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/droppable.directive.jsheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/co0000664000175000017500000000520213737340771034003 0ustar zuulzuul00000000000000(function() { 'use strict'; angular.module('horizon.dashboard.project.heat_dashboard.template_generator').directive('droppable', ['hotgenGlobals', 'hotgenUUID', 'hotgenStates', 'horizon.dashboard.project.heat_dashboard.template_generator.basePath', function(hotgenGlobals, hotgenUUID, hotgenStates, basePath){ return { link: function ($scope, element){ $scope.data = { nodes: hotgenStates.get_nodes(), edges: hotgenStates.get_edges(), } $scope.dropHandler = function(e){ var resource_types = hotgenGlobals.get_resource_icons(); var dropped_elem_id = e.dataTransfer.getData("text"); var dropped_elem_base = document.getElementById(dropped_elem_id); var resource_type = dropped_elem_id; var shorten_resource_type = resource_type.split('__')[2] var dragged_resource = resource_types[resource_type]; var counter = hotgenStates.increment_counter(shorten_resource_type); var id = hotgenUUID.uuid(); var node_label = shorten_resource_type+'_'+counter; hotgenStates.set_incremented_label(id, node_label) $scope.data.nodes.add({ id: id, label: node_label, shape: 'circularImage', title: resource_type, icon: { face: 'FontAwesome', code: dragged_resource.code, size: 50, color: dragged_resource.color, }, borderWidth: 0, borderWidthSelected: 0, color: {border: '#ffffff', background: '#ffffff', highlight: '#ffffff', hover: '#ffffff'}, image: basePath+'js/resources/'+resource_type.toLowerCase()+'/'+resource_type.toLowerCase()+'-gray.svg', }); hotgenStates.update_saved_flags(id, false) e.preventDefault(); } $scope.dragoverHandler = function(e){ if (e.preventDefault){ e.preventDefault(); } } var el = element[0]; el.addEventListener('dragover', $scope.dragoverHandler, true); el.addEventListener('drop', $scope.dropHandler, false); } } }]); })();heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/0000775000175000017500000000000013737341153033627 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000016700000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/angular-notify.scssheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/a0000664000175000017500000000305213737340771033777 0ustar zuulzuul00000000000000.cg-notify-message { position:fixed; top:0px; z-index: 9999; max-width:400px; text-align: center; background-color: #d9edf7; color: #31708f; padding: 15px; border: 1px solid #bce8f1; border-radius: 4px; -webkit-transition: top 0.5s ease-out,opacity 0.2s ease-out; -moz-transition: top 0.5s ease-out,opacity 0.2s ease-out; -o-transition: top 0.5s ease-out,opacity 0.2s ease-out; transition: top 0.5s ease-out,opacity 0.2s ease-out; visibility:hidden; -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); box-shadow: 0 6px 12px rgba(0,0,0,.175); } .cg-notify-message-center { left:50%; } .cg-notify-message-left { left:15px; } .cg-notify-message-right { right:15px; } .cg-notify-message a { font-weight:bold; color:inherit; } .cg-notify-message a:hover { color:inherit; } .cg-notify-close { -webkit-appearance: none; padding: 0; cursor: pointer; background: 0 0; border: 0; font-size: 21px; font-weight: 700; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; filter: alpha(opacity=20); opacity: .2; position: absolute; top: 0px; right: 3px; line-height: 15px; } .cg-notify-close:hover, .cg-notify-close:focus { color: #000; text-decoration: none; cursor: pointer; filter: alpha(opacity=50); opacity: .5; } .cg-notify-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }././@LongLink0000000000000000000000000000015000000000000011211 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000775000175000017500000000000013737341153034000 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000015600000000000011217 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000775000175000017500000000000013737341153034000 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000017500000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/unknown-red.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000437613737340771034021 0ustar zuulzuul00000000000000 ]> ././@LongLink0000000000000000000000000000017600000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/unknown-gray.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000437613737340771034021 0ustar zuulzuul00000000000000 ]> ././@LongLink0000000000000000000000000000017100000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/unknown.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000437613737340771034021 0ustar zuulzuul00000000000000 ]> ././@LongLink0000000000000000000000000000017100000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/spinner.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000640413737340771034013 0ustar zuulzuul00000000000000 Layer 1././@LongLink0000000000000000000000000000017200000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/floppy-o.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000145413737340771034013 0ustar zuulzuul00000000000000 ././@LongLink0000000000000000000000000000017500000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/file-text-o.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000167713737340771034022 0ustar zuulzuul00000000000000 ././@LongLink0000000000000000000000000000017700000000000011222 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/ic_close_24px.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000031213737340771034003 0ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000016700000000000011221 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/trash.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000202513737340771034006 0ustar zuulzuul00000000000000 ././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/ic_help_outline_24px.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000060313737340771034006 0ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000017700000000000011222 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/img/icons/unknown-green.svgheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/i0000664000175000017500000000437613737340771034021 0ustar zuulzuul00000000000000 ]> ././@LongLink0000000000000000000000000000015400000000000011215 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/vis.scssheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/v0000664000175000017500000006642413737340771034040 0ustar zuulzuul00000000000000.vis .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; /* Must be displayed above for example selected Timeline items */ z-index: 10; } .vis-active { box-shadow: 0 0 10px #86d5f8; } /* override some bootstrap styles screwing up the timelines css */ .vis [class*="span"] { min-height: 0; width: auto; } .vis-timeline { position: relative; border: 1px solid #bfbfbf; overflow: hidden; padding: 0; margin: 0; box-sizing: border-box; } .vis-panel { position: absolute; padding: 0; margin: 0; box-sizing: border-box; } .vis-panel.vis-center, .vis-panel.vis-left, .vis-panel.vis-right, .vis-panel.vis-top, .vis-panel.vis-bottom { border: 1px #bfbfbf; } .vis-panel.vis-center, .vis-panel.vis-left, .vis-panel.vis-right { border-top-style: solid; border-bottom-style: solid; overflow: hidden; } .vis-panel.vis-center, .vis-panel.vis-top, .vis-panel.vis-bottom { border-left-style: solid; border-right-style: solid; } .vis-background { overflow: hidden; } .vis-panel > .vis-content { position: relative; } .vis-panel .vis-shadow { position: absolute; width: 100%; height: 1px; box-shadow: 0 0 10px rgba(0,0,0,0.8); /* TODO: find a nice way to ensure vis-shadows are drawn on top of items z-index: 1; */ } .vis-panel .vis-shadow.vis-top { top: -1px; left: 0; } .vis-panel .vis-shadow.vis-bottom { bottom: -1px; left: 0; } .vis-labelset { position: relative; overflow: hidden; box-sizing: border-box; } .vis-labelset .vis-label { position: relative; left: 0; top: 0; width: 100%; color: #4d4d4d; box-sizing: border-box; } .vis-labelset .vis-label { border-bottom: 1px solid #bfbfbf; } .vis-labelset .vis-label:last-child { border-bottom: none; } .vis-labelset .vis-label .vis-inner { display: inline-block; padding: 5px; } .vis-labelset .vis-label .vis-inner.vis-hidden { padding: 0; } .vis-itemset { position: relative; padding: 0; margin: 0; box-sizing: border-box; } .vis-itemset .vis-background, .vis-itemset .vis-foreground { position: absolute; width: 100%; height: 100%; overflow: visible; } .vis-axis { position: absolute; width: 100%; height: 0; left: 0; z-index: 1; } .vis-foreground .vis-group { position: relative; box-sizing: border-box; border-bottom: 1px solid #bfbfbf; } .vis-foreground .vis-group:last-child { border-bottom: none; } .vis-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; } .vis-item { position: absolute; color: #1A1A1A; border-color: #97B0F8; border-width: 1px; background-color: #D5DDF6; display: inline-block; /*overflow: hidden;*/ } .vis-item.vis-selected { border-color: #FFC200; background-color: #FFF785; /* z-index must be higher than the z-index of custom time bar and current time bar */ z-index: 2; } .vis-editable.vis-selected { cursor: move; } .vis-item.vis-point.vis-selected { background-color: #FFF785; } .vis-item.vis-box { text-align: center; border-style: solid; border-radius: 2px; } .vis-item.vis-point { background: none; } .vis-item.vis-dot { position: absolute; padding: 0; border-width: 4px; border-style: solid; border-radius: 4px; } .vis-item.vis-range { border-style: solid; border-radius: 2px; box-sizing: border-box; } .vis-item.vis-background { border: none; background-color: rgba(213, 221, 246, 0.4); box-sizing: border-box; padding: 0; margin: 0; } .vis-item .vis-item-overflow { position: relative; width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; } .vis-item.vis-range .vis-item-content { position: relative; display: inline-block; } .vis-item.vis-background .vis-item-content { position: absolute; display: inline-block; } .vis-item.vis-line { padding: 0; position: absolute; width: 0; border-left-width: 1px; border-left-style: solid; } .vis-item .vis-item-content { white-space: nowrap; box-sizing: border-box; padding: 5px; } .vis-item .vis-delete { background: none; position: absolute; width: 24px; height: 24px; top: -4px; right: -24px; cursor: pointer; } .vis-item.vis-range .vis-drag-left { position: absolute; width: 24px; max-width: 20%; min-width: 2px; height: 100%; top: 0; left: -4px; cursor: w-resize; } .vis-item.vis-range .vis-drag-right { position: absolute; width: 24px; max-width: 20%; min-width: 2px; height: 100%; top: 0; right: -4px; cursor: e-resize; } .vis-time-axis { position: relative; overflow: hidden; } .vis-time-axis.vis-foreground { top: 0; left: 0; width: 100%; } .vis-time-axis.vis-background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .vis-time-axis .vis-text { position: absolute; color: #4d4d4d; padding: 3px; white-space: nowrap; } .vis-time-axis .vis-text.vis-measure { position: absolute; padding-left: 0; padding-right: 0; margin-left: 0; margin-right: 0; visibility: hidden; } .vis-time-axis .vis-grid.vis-vertical { position: absolute; border-left: 1px solid; } .vis-time-axis .vis-grid.vis-minor { border-color: #e5e5e5; } .vis-time-axis .vis-grid.vis-major { border-color: #bfbfbf; } .vis-current-time { background-color: #FF7F6E; width: 2px; z-index: 1; } .vis-custom-time { background-color: #6E94FF; width: 2px; cursor: move; z-index: 1; } .vis-timeline { /* -webkit-transition: height .4s ease-in-out; transition: height .4s ease-in-out; */ } .vis-panel { /* -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; transition: height .4s ease-in-out, top .4s ease-in-out; */ } .vis-axis { /* -webkit-transition: top .4s ease-in-out; transition: top .4s ease-in-out; */ } /* TODO: get animation working nicely .vis-item { -webkit-transition: top .4s ease-in-out; transition: top .4s ease-in-out; } .vis-item.line { -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; transition: height .4s ease-in-out, top .4s ease-in-out; } /**/ .vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal { position: absolute; width: 100%; height: 0; border-bottom: 1px solid; } .vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor { border-color: #e5e5e5; } .vis-panel.vis-background.vis-horizontal .vis-grid.vis-major { border-color: #bfbfbf; } .vis-data-axis .vis-y-axis.vis-major { width: 100%; position: absolute; color: #4d4d4d; white-space: nowrap; } .vis-data-axis .vis-y-axis.vis-major.vis-measure { padding: 0; margin: 0; border: 0; visibility: hidden; width: auto; } .vis-data-axis .vis-y-axis.vis-minor { position: absolute; width: 100%; color: #bebebe; white-space: nowrap; } .vis-data-axis .vis-y-axis.vis-minor.vis-measure { padding: 0; margin: 0; border: 0; visibility: hidden; width: auto; } .vis-data-axis .vis-y-axis.vis-title { position: absolute; color: #4d4d4d; white-space: nowrap; bottom: 20px; text-align: center; } .vis-data-axis .vis-y-axis.vis-title.vis-measure { padding: 0; margin: 0; visibility: hidden; width: auto; } .vis-data-axis .vis-y-axis.vis-title.vis-left { bottom: 0; -webkit-transform-origin: left top; -moz-transform-origin: left top; -ms-transform-origin: left top; -o-transform-origin: left top; transform-origin: left bottom; -webkit-transform: rotate(-90deg); -moz-transform: rotate(-90deg); -ms-transform: rotate(-90deg); -o-transform: rotate(-90deg); transform: rotate(-90deg); } .vis-data-axis .vis-y-axis.vis-title.vis-right { bottom: 0; -webkit-transform-origin: right bottom; -moz-transform-origin: right bottom; -ms-transform-origin: right bottom; -o-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); -o-transform: rotate(90deg); transform: rotate(90deg); } .vis-legend { background-color: rgba(247, 252, 255, 0.65); padding: 5px; border: 1px solid #b3b3b3; box-shadow: 2px 2px 10px rgba(154, 154, 154, 0.55); } .vis-legend-text { /*font-size: 10px;*/ white-space: nowrap; display: inline-block } .vis-graph-group0 { fill:#4f81bd; fill-opacity:0; stroke-width:2px; stroke: #4f81bd; } .vis-graph-group1 { fill:#f79646; fill-opacity:0; stroke-width:2px; stroke: #f79646; } .vis-graph-group2 { fill: #8c51cf; fill-opacity:0; stroke-width:2px; stroke: #8c51cf; } .vis-graph-group3 { fill: #75c841; fill-opacity:0; stroke-width:2px; stroke: #75c841; } .vis-graph-group4 { fill: #ff0100; fill-opacity:0; stroke-width:2px; stroke: #ff0100; } .vis-graph-group5 { fill: #37d8e6; fill-opacity:0; stroke-width:2px; stroke: #37d8e6; } .vis-graph-group6 { fill: #042662; fill-opacity:0; stroke-width:2px; stroke: #042662; } .vis-graph-group7 { fill:#00ff26; fill-opacity:0; stroke-width:2px; stroke: #00ff26; } .vis-graph-group8 { fill:#ff00ff; fill-opacity:0; stroke-width:2px; stroke: #ff00ff; } .vis-graph-group9 { fill: #8f3938; fill-opacity:0; stroke-width:2px; stroke: #8f3938; } .vis-timeline .vis-fill { fill-opacity:0.1; stroke: none; } .vis-timeline .vis-bar { fill-opacity:0.5; stroke-width:1px; } .vis-timeline .vis-point { stroke-width:2px; fill-opacity:1.0; } .vis-timeline .vis-legend-background { stroke-width:1px; fill-opacity:0.9; fill: #ffffff; stroke: #c2c2c2; } .vis-timeline .vis-outline { stroke-width:1px; fill-opacity:1; fill: #ffffff; stroke: #e5e5e5; } .vis-timeline .vis-icon-fill { fill-opacity:0.3; stroke: none; } div.vis-network div.vis-manipulation { border-width: 0; border-bottom: 1px; border-style:solid; border-color: #d6d9d8; background: #ffffff; /* Old browsers */ background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ position: absolute; left: 0; top: 1%; width: 100%; height: 30px; } div.vis-network div.vis-edit-mode { position:absolute; left: 0; top: 15px; height: 30px; } /* FIXME: shouldn't the vis-close button be a child of the vis-manipulation div? */ div.vis-network div.vis-close { position:absolute; right: 0; top: 0; width: 30px; height: 30px; background-position: 20px 3px; background-repeat: no-repeat; background-image: none; cursor: pointer; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } div.vis-network div.vis-close:hover { opacity: 0.6; } div.vis-network div.vis-manipulation div.vis-button, div.vis-network div.vis-edit-mode div.vis-button { position:relative; top:-7px; font-family: verdana; font-size: 12px; -moz-border-radius: 15px; border-radius: 15px; display:inline-block; background-position: 0px 0px; background-repeat:no-repeat; height:24px; margin: 0px 0px 0px 10px; vertical-align:middle; cursor: pointer; padding: 0px 8px 0px 8px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } div.vis-network div.vis-manipulation div.vis-button:hover { box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); } div.vis-network div.vis-manipulation div.vis-button:active { box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); } div.vis-network div.vis-manipulation div.vis-button.vis-back { background-image: none; } div.vis-network div.vis-manipulation div.vis-button.vis-none:hover { box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); cursor: default; } div.vis-network div.vis-manipulation div.vis-button.vis-none:active { box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); } div.vis-network div.vis-manipulation div.vis-button.vis-none { padding: 0; } div.vis-network div.vis-manipulation div.notification { margin: 2px; font-weight: bold; } div.vis-network div.vis-manipulation div.vis-button.vis-add { background-image: none; } div.vis-network div.vis-manipulation div.vis-button.vis-edit, div.vis-network div.vis-edit-mode div.vis-button.vis-edit { background-image: none; } div.vis-network div.vis-edit-mode div.vis-button.vis-edit.vis-edit-mode { background-color: #fcfcfc; border: 1px solid #cccccc; } div.vis-network div.vis-manipulation div.vis-button.vis-connect { background-image: none; } div.vis-network div.vis-manipulation div.vis-button.vis-delete { background-image: none; } /* top right bottom left */ div.vis-network div.vis-manipulation div.vis-label, div.vis-network div.vis-edit-mode div.vis-label { margin: 0 5px; line-height: 25px; } div.vis-network div.vis-manipulation div.vis-separator-line { display: inline-block; width: 0px; height: 20px; background-color: #bdbdbd; margin: 5px 7px 0 15px; } /* TODO: is this redundant? div.network-navigation_wrapper { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } */ div.vis-network div.vis-navigation div.vis-button { width:34px; height:34px; -moz-border-radius: 17px; border-radius: 17px; position:absolute; display:inline-block; background-position: 2px 2px; background-repeat:no-repeat; cursor: pointer; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } div.vis-network div.vis-navigation div.vis-button:hover { box-shadow: 0 0 3px 3px rgba(56, 207, 21, 0.30); } div.vis-network div.vis-navigation div.vis-button:active { box-shadow: 0 0 1px 3px rgba(56, 207, 21, 0.95); } div.vis-network div.vis-navigation div.vis-button.vis-up { background-image: none; bottom:50px; left:55px; } div.vis-network div.vis-navigation div.vis-button.vis-down { background-image: none; bottom:10px; left:55px; } div.vis-network div.vis-navigation div.vis-button.vis-left { background-image: none; bottom:10px; left:15px; } div.vis-network div.vis-navigation div.vis-button.vis-right { background-image: none; bottom:10px; left:95px; } div.vis-network div.vis-navigation div.vis-button.vis-zoomIn { background-image: none; bottom:10px; right:15px; } div.vis-network div.vis-navigation div.vis-button.vis-zoomOut { background-image: none; bottom:10px; right:55px; } div.vis-network div.vis-navigation div.vis-button.vis-zoomExtends { background-image: none; bottom:50px; right:15px; } div.vis-network-tooltip { position: absolute; visibility: hidden; padding: 5px; white-space: nowrap; font-family: verdana; font-size:14px; font-color:#000000; background-color: #f5f4ed; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; border: 1px solid #808074; box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2); pointer-events: none; } div.vis-network-configuration { position:relative; display:block; float:left; font-size:12px; } div.vis-network-configuration-wrapper { display:block; width:700px; } div.vis-network-configuration.vis-option-container{ display:block; width:495px; background-color: #ffffff; border:2px solid #f7f8fa; border-radius:4px; margin-top:20px; left:10px; padding-left:5px; } div.vis-network-configuration.button{ display:block; width:495px; height:25px; vertical-align: middle; line-height:25px; background-color: #f7f8fa; border:2px solid #ceced0; border-radius:4px; margin-top:20px; left:10px; padding-left:5px; cursor: pointer; margin-bottom:30px; } div.vis-network-configuration.button.hover{ background-color: #4588e6; border:2px solid #214373; color:#ffffff; } div.vis-network-configuration.item{ display:block; float:left; width:495px; height:25px; vertical-align: middle; line-height:25px; } div.vis-network-configuration.item.s2{ left:10px; background-color: #f7f8fa; padding-left:5px; border-radius:3px; } div.vis-network-configuration.item.s3{ left:20px; background-color: #e4e9f0; padding-left:5px; border-radius:3px; } div.vis-network-configuration.item.s4{ left:30px; background-color: #cfd8e6; padding-left:5px; border-radius:3px; } div.vis-network-configuration.header{ font-size:18px; font-weight: bold; } div.vis-network-configuration.label{ width:120px; height:25px; line-height: 25px; } div.vis-network-configuration.label.s3{ width:110px; } div.vis-network-configuration.label.s4{ width:100px; } div.vis-network-configuration.colorBlock{ top:1px; width:30px; height:19px; border:1px solid #444444; border-radius:2px; padding:0px; margin:0px; cursor:pointer; } input.vis-network-configuration.checkbox { left:-5px; } input.vis-network-configuration.rangeinput{ position:relative; top:-5px; width:60px; height:13px; padding:1px; margin:0; pointer-events:none; } input.vis-network-configuration.range{ /*removes default webkit styles*/ -webkit-appearance: none; /*fix for FF unable to apply focus style bug */ border: 0px solid white; background-color:rgba(0,0,0,0); /*required for proper track sizing in FF*/ width: 300px; height:20px; } input.vis-network-configuration.range::-webkit-slider-runnable-track { width: 300px; height: 5px; background: #dedede; /* Old browsers */ background: -moz-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dedede), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* IE10+ */ background: linear-gradient(to bottom, #dedede 0%,#c8c8c8 99%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ border: 1px solid #999999; box-shadow: #aaaaaa 0px 0px 3px 0px; border-radius: 3px; } input.vis-network-configuration.range::-webkit-slider-thumb { -webkit-appearance: none; border: 1px solid #14334b; height: 17px; width: 17px; border-radius: 50%; background: #3876c2; /* Old browsers */ background: -moz-linear-gradient(top, #3876c2 0%, #385380 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#3876c2), color-stop(100%,#385380)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #3876c2 0%,#385380 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #3876c2 0%,#385380 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #3876c2 0%,#385380 100%); /* IE10+ */ background: linear-gradient(to bottom, #3876c2 0%,#385380 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3876c2', endColorstr='#385380',GradientType=0 ); /* IE6-9 */ box-shadow: #111927 0px 0px 1px 0px; margin-top: -7px; } input.vis-network-configuration.range:focus { outline: none; } input.vis-network-configuration.range:focus::-webkit-slider-runnable-track { background: #9d9d9d; /* Old browsers */ background: -moz-linear-gradient(top, #9d9d9d 0%, #c8c8c8 99%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#9d9d9d), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* IE10+ */ background: linear-gradient(to bottom, #9d9d9d 0%,#c8c8c8 99%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9d9d9d', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ } input.vis-network-configuration.range::-moz-range-track { width: 300px; height: 10px; background: #dedede; /* Old browsers */ background: -moz-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dedede), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* IE10+ */ background: linear-gradient(to bottom, #dedede 0%,#c8c8c8 99%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ border: 1px solid #999999; box-shadow: #aaaaaa 0px 0px 3px 0px; border-radius: 3px; } input.vis-network-configuration.range::-moz-range-thumb { border: none; height: 16px; width: 16px; border-radius: 50%; background: #385380; } /*hide the outline behind the border*/ input.vis-network-configuration.range:-moz-focusring{ outline: 1px solid white; outline-offset: -1px; } input.vis-network-configuration.range::-ms-track { width: 300px; height: 5px; /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */ background: transparent; /*leave room for the larger thumb to overflow with a transparent border */ border-color: transparent; border-width: 6px 0; /*remove default tick marks*/ color: transparent; } input.vis-network-configuration.range::-ms-fill-lower { background: #777; border-radius: 10px; } input.vis-network-configuration.range::-ms-fill-upper { background: #ddd; border-radius: 10px; } input.vis-network-configuration.range::-ms-thumb { border: none; height: 16px; width: 16px; border-radius: 50%; background: #385380; } input.vis-network-configuration.range:focus::-ms-fill-lower { background: #888; } input.vis-network-configuration.range:focus::-ms-fill-upper { background: #ccc; } div.vis-color-picker { position:absolute; margin-top:-140px; margin-left:30px; width:293px; height:425px; padding: 10px; border-radius:15px; background-color:#ffffff; display:none; box-shadow: rgba(0,0,0,0.5) 0px 0px 10px 0px; } div.vis-color-picker div.vis-arrow { position: absolute; top:147px; left:5px; } div.vis-color-picker div.vis-arrow:after, div.vis-color-picker div.vis-arrow:before { right: 100%; top: 50%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } div.vis-color-picker div.vis-arrow:after { border-color: rgba(255, 255, 255, 0); border-right-color: #ffffff; border-width: 30px; margin-top: -30px; } div.vis-color-picker div.vis-color { position:absolute; width: 289px; height: 289px; cursor: pointer; } div.vis-color-picker div.vis-brightness { position: absolute; top:313px; } div.vis-color-picker div.vis-opacity { position:absolute; top:350px; } div.vis-color-picker div.vis-selector { position:absolute; top:137px; left:137px; width:15px; height:15px; border-radius:15px; border:1px solid #ffffff; background: #4c4c4c; /* Old browsers */ background: -moz-linear-gradient(top, #4c4c4c 0%, #595959 12%, #666666 25%, #474747 39%, #2c2c2c 50%, #000000 51%, #111111 60%, #2b2b2b 76%, #1c1c1c 91%, #131313 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(12%,#595959), color-stop(25%,#666666), color-stop(39%,#474747), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(60%,#111111), color-stop(76%,#2b2b2b), color-stop(91%,#1c1c1c), color-stop(100%,#131313)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* IE10+ */ background: linear-gradient(to bottom, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ } div.vis-color-picker div.vis-new-color { position:absolute; width:140px; height:20px; border:1px solid rgba(0,0,0,0.1); border-radius:5px; top:380px; left:159px; text-align:right; padding-right:2px; font-size:10px; color:rgba(0,0,0,0.4); vertical-align:middle; line-height:20px; } div.vis-color-picker div.vis-initial-color { position:absolute; width:140px; height:20px; border:1px solid rgba(0,0,0,0.1); border-radius:5px; top:380px; left:10px; text-align:left; padding-left:2px; font-size:10px; color:rgba(0,0,0,0.4); vertical-align:middle; line-height:20px; } div.vis-color-picker div.vis-label { position:absolute; width:300px; left:10px; } div.vis-color-picker div.vis-label.vis-brightness { top:300px; } div.vis-color-picker div.vis-label.vis-opacity { top:338px; } div.vis-color-picker div.vis-button { position:absolute; width:68px; height:25px; border-radius:10px; vertical-align: middle; text-align:center; line-height: 25px; top:410px; border:2px solid #d9d9d9; background-color: #f7f7f7; cursor:pointer; } div.vis-color-picker div.vis-button.vis-cancel { /*border:2px solid #ff4e33;*/ /*background-color: #ff7761;*/ left:5px; } div.vis-color-picker div.vis-button.vis-load { /*border:2px solid #a153e6;*/ /*background-color: #cb8dff;*/ left:82px; } div.vis-color-picker div.vis-button.vis-apply { /*border:2px solid #4588e6;*/ /*background-color: #82b6ff;*/ left:159px; } div.vis-color-picker div.vis-button.vis-save { /*border:2px solid #45e655;*/ /*background-color: #6dff7c;*/ left:236px; } div.vis-color-picker input.vis-range { width: 290px; height:20px; } /* TODO: is this redundant? div.vis-color-picker input.vis-range-brightness { width: 289px !important; } div.vis-color-picker input.vis-saturation-range { width: 289px !important; }*/ ././@LongLink0000000000000000000000000000015700000000000011220 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/hotgen.scssheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/h0000664000175000017500000000726113737340771034014 0ustar zuulzuul00000000000000#canvas_area { /* background-image: url("../img/canvasbg.jpg"); */ border: 1px solid #cccccc; width: 100%; height: 100%; -moz-border-radius: 0.5em; border-radius: 0.5em; text-align: left; } .table_action { padding: 5px; width: 100%; text-align: right; } div.vis-button.vis-back, div.vis-button.vis-connect, div.vis-button.vis-delete, div.vis-button.vis-edit { border: medium none!important; box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4)!important; text-transform: uppercase!important; transition: all 0.4s ease 0s!important; border-color: transparent!important; color: #444!important; -moz-user-select: none!important; border-radius: 4px!important; cursor: pointer!important; display: inline-block!important; font-size: 12px!important; font-weight: 400!important; line-height: 1.42857!important; margin-top: 10px!important; margin-bottom: 25px!important; padding: 6px 6px!important; text-align: center!important; touch-action: manipulation!important; vertical-align: middle!important; white-space: nowrap!important; } div.vis-button.vis-back div.vis-label, div.vis-button.vis-connect div.vis-label, div.vis-button.vis-delete div.vis-label, div.vis-button.vis-edit div.vis-label { margin-top: -7px!important; } div.vis-network div.vis-manipulation div.vis-label, div.vis-network div.vis-edit-mode div.vis-label{ margin: -5px 15px 10px 25px; } div.vis-network div.vis-manipulation div.vis-button.vis-none { margin-top: -15px; } div.vis-network div.vis-manipulation div.vis-separator-line { width: 0px } .mat-tab-links { display: table; width: 100%; .mat-tab-link { display: table-cell; } } md-content { min-height: 500px } md-input-container{ margin: 25px 0 0 0; } .os-resource{ auto:overflow; max-height:500px; margin: 10px; display:inline-block; } md-pagination-wrapper{ width:100%; } .odd { background-color:#fff; margin: 10px 0px; } .even { background-color:#fff; margin: 10px 0px; } .md-chips md-chip{ border-radius:3px; background-color:white; border: 1px solid rgb(63,81,181) } .animate-show-hide.ng-hide { opacity: 0; } .animate-show-hide.ng-hide-add, .animate-show-hide.ng-hide-remove { transition: all linear 0.5s; } div.load-block{ height: 70%; width: 90%; font-size: 32px; text-align: center; padding: 25% 0%; position: absolute; z-index: 100; border-radius: 20px; background-color: rgba(128, 128, 128, 0.5); } md-icon.spinner{ animation: rotate 1.4s linear infinite; -webkit-animation: rotate 1.4s linear infinite; -moz-animation: rotate 1.4s linear infinite; height:60px; width:60px; } @keyframes rotate { to { transform: rotate(360deg); } } @-webkit-keyframes rotate { to { -webkit-transform: rotate(360deg); } } @-moz-keyframes rotate { to { transform: rotate(360deg); } } .cg-notify-message.alert-success{ background-color: #dff0d8; } .cg-notify-message.alert-danger{ color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .cg-notify-message.alert-warning{ color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .cg-notify-message.alert-info{ color: #d9edf7; background-color: #d9edf7; border-color: #bce8f1; } .mat-tooltip{ font-size: 12px!important } .md-toolbar-tools h2{ color: #fff; } .div-scroll{ overflow: auto; max-height: 500px; } .md-nav-bar { border-style: none; border-width: 0 0; position: absolute; left: 50%; transform: translate(-50%, -50%); } .navbar-content{ margin-top: 3%; text-align: center; } ././@LongLink0000000000000000000000000000016400000000000011216 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/hotgen-main.scssheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/h0000664000175000017500000000053313737340771034007 0ustar zuulzuul00000000000000// My Themes @import "/dashboard/project/heat_dashboard/template_generator/css/angular-material.scss"; @import "/dashboard/project/heat_dashboard/template_generator/css/angular-notify.scss"; @import "/dashboard/project/heat_dashboard/template_generator/css/hotgen.scss"; @import "/dashboard/project/heat_dashboard/template_generator/css/vis.scss";././@LongLink0000000000000000000000000000017100000000000011214 Lustar 00000000000000heat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/angular-material.scssheat-dashboard-3.0.1/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/css/a0000664000175000017500000203720613737340771034011 0ustar zuulzuul00000000000000/*! * AngularJS Material Design * https://github.com/angular/material * @license MIT * v1.1.5 */ html, body { height: 100%; position: relative; } body { margin: 0; padding: 0; } [tabindex='-1']:focus { outline: none; } .inset { padding: 10px; } a.md-no-style, button.md-no-style { font-weight: normal; background-color: inherit; text-align: left; border: none; padding: 0; margin: 0; } select, button, textarea, input { vertical-align: baseline; } input[type="reset"], input[type="submit"], html input[type="button"], button { cursor: pointer; -webkit-appearance: button; } input[type="reset"][disabled], input[type="submit"][disabled], html input[type="button"][disabled], button[disabled] { cursor: default; } textarea { vertical-align: top; overflow: auto; } input[type="search"] { -webkit-appearance: textfield; box-sizing: content-box; -webkit-box-sizing: content-box; } input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } input:-webkit-autofill { text-shadow: none; } .md-visually-hidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; text-transform: none; width: 1px; } .md-shadow { position: absolute; top: 0; left: 0; bottom: 0; right: 0; border-radius: inherit; pointer-events: none; } .md-shadow-bottom-z-1 { box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); } .md-shadow-bottom-z-2 { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4); } .md-shadow-animated.md-shadow { -webkit-transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); } /* * A container inside of a rippling element (eg a button), * which contains all of the individual ripples */ .md-ripple-container { pointer-events: none; position: absolute; overflow: hidden; left: 0; top: 0; width: 100%; height: 100%; -webkit-transition: all 0.55s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.55s cubic-bezier(0.25, 0.8, 0.25, 1); } .md-ripple { position: absolute; -webkit-transform: translate(-50%, -50%) scale(0); transform: translate(-50%, -50%) scale(0); -webkit-transform-origin: 50% 50%; transform-origin: 50% 50%; opacity: 0; border-radius: 50%; } .md-ripple.md-ripple-placed { -webkit-transition: margin 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), border 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), width 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), height 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), -webkit-transform 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); transition: margin 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), border 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), width 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), height 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), -webkit-transform 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); transition: margin 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), border 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), width 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), height 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); transition: margin 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), border 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), width 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), height 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.9s cubic-bezier(0.25, 0.8, 0.25, 1), -webkit-transform 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); } .md-ripple.md-ripple-scaled { -webkit-transform: translate(-50%, -50%) scale(1); transform: translate(-50%, -50%) scale(1); } .md-ripple.md-ripple-active, .md-ripple.md-ripple-full, .md-ripple.md-ripple-visible { opacity: 0.20; } .md-ripple.md-ripple-remove { -webkit-animation: md-remove-ripple 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); animation: md-remove-ripple 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); } @-webkit-keyframes md-remove-ripple { 0% { opacity: .15; } 100% { opacity: 0; } } @keyframes md-remove-ripple { 0% { opacity: .15; } 100% { opacity: 0; } } .md-padding { padding: 8px; } .md-margin { margin: 8px; } .md-scroll-mask { position: absolute; background-color: transparent; top: 0; right: 0; bottom: 0; left: 0; z-index: 50; } .md-scroll-mask > .md-scroll-mask-bar { display: block; position: absolute; background-color: #fafafa; right: 0; top: 0; bottom: 0; z-index: 65; box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 0.3); } .md-no-momentum { -webkit-overflow-scrolling: auto; } .md-no-flicker { -webkit-filter: blur(0px); } @media (min-width: 960px) { .md-padding { padding: 16px; } } html[dir=rtl], html[dir=ltr], body[dir=rtl], body[dir=ltr] { unicode-bidi: embed; } bdo[dir=rtl] { direction: rtl; unicode-bidi: bidi-override; } bdo[dir=ltr] { direction: ltr; unicode-bidi: bidi-override; } html, body { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; min-height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /************ * Headings ************/ .md-display-4 { font-size: 112px; font-weight: 300; letter-spacing: -0.010em; line-height: 112px; } .md-display-3 { font-size: 56px; font-weight: 400; letter-spacing: -0.005em; line-height: 56px; } .md-display-2 { font-size: 45px; font-weight: 400; line-height: 64px; } .md-display-1 { font-size: 34px; font-weight: 400; line-height: 40px; } .md-headline { font-size: 24px; font-weight: 400; line-height: 32px; } .md-title { font-size: 20px; font-weight: 500; letter-spacing: 0.005em; } .md-subhead { font-size: 16px; font-weight: 400; letter-spacing: 0.010em; line-height: 24px; } /************ * Body Copy ************/ .md-body-1 { font-size: 14px; font-weight: 400; letter-spacing: 0.010em; line-height: 20px; } .md-body-2 { font-size: 14px; font-weight: 500; letter-spacing: 0.010em; line-height: 24px; } .md-caption { font-size: 12px; letter-spacing: 0.020em; } .md-button { letter-spacing: 0.010em; } /************ * Defaults ************/ button, select, html, textarea, input { font-family: Roboto, "Helvetica Neue", sans-serif; } select, button, textarea, input { font-size: 100%; } /* * * Responsive attributes * * References: * 1) https://scotch.io/tutorials/a-visual-guide-to-css3-flexbox-properties#flex * 2) https://css-tricks.com/almanac/properties/f/flex/ * 3) https://css-tricks.com/snippets/css/a-guide-to-flexbox/ * 4) https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items * 5) http://godban.com.ua/projects/flexgrid * * */ .md-panel-outer-wrapper { height: 100%; left: 0; position: absolute; top: 0; width: 100%; } ._md-panel-hidden { display: none; } ._md-panel-offscreen { left: -9999px; } ._md-panel-fullscreen { border-radius: 0; left: 0; min-height: 100%; min-width: 100%; position: fixed; top: 0; } ._md-panel-shown .md-panel { opacity: 1; -webkit-transition: none; transition: none; } .md-panel { opacity: 0; position: fixed; } .md-panel._md-panel-shown { opacity: 1; -webkit-transition: none; transition: none; } .md-panel._md-panel-animate-enter { opacity: 1; -webkit-transition: all 0.3s cubic-bezier(0, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0, 0, 0.2, 1); } .md-panel._md-panel-animate-leave { opacity: 1; -webkit-transition: all 0.3s cubic-bezier(0.4, 0, 1, 1); transition: all 0.3s cubic-bezier(0.4, 0, 1, 1); } .md-panel._md-panel-animate-scale-out, .md-panel._md-panel-animate-fade-out { opacity: 0; } .md-panel._md-panel-backdrop { height: 100%; position: absolute; width: 100%; } .md-panel._md-opaque-enter { opacity: .48; -webkit-transition: opacity 0.3s cubic-bezier(0, 0, 0.2, 1); transition: opacity 0.3s cubic-bezier(0, 0, 0.2, 1); } .md-panel._md-opaque-leave { -webkit-transition: opacity 0.3s cubic-bezier(0.4, 0, 1, 1); transition: opacity 0.3s cubic-bezier(0.4, 0, 1, 1); } md-autocomplete { border-radius: 2px; display: block; height: 40px; position: relative; overflow: visible; min-width: 190px; } md-autocomplete[disabled] input { cursor: default; } md-autocomplete[md-floating-label] { border-radius: 0; background: transparent; height: auto; } md-autocomplete[md-floating-label] md-input-container { padding-bottom: 0; } md-autocomplete[md-floating-label] md-autocomplete-wrap { height: auto; } md-autocomplete[md-floating-label] .md-show-clear-button button { display: block; position: absolute; right: 0; top: 20px; width: 30px; height: 30px; } md-autocomplete[md-floating-label] .md-show-clear-button input { padding-right: 30px; } [dir=rtl] md-autocomplete[md-floating-label] .md-show-clear-button input { padding-right: 0; padding-left: 30px; } md-autocomplete md-autocomplete-wrap { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; box-sizing: border-box; position: relative; overflow: visible; height: 40px; } md-autocomplete md-autocomplete-wrap.md-menu-showing { z-index: 51; } md-autocomplete md-autocomplete-wrap md-input-container, md-autocomplete md-autocomplete-wrap input { -webkit-box-flex: 1; -webkit-flex: 1 1 0%; flex: 1 1 0%; box-sizing: border-box; min-width: 0; } md-autocomplete md-autocomplete-wrap md-progress-linear { position: absolute; bottom: -2px; left: 0; } md-autocomplete md-autocomplete-wrap md-progress-linear.md-inline { bottom: 40px; right: 2px; left: 2px; width: auto; } md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate { position: absolute; top: 0; left: 0; width: 100%; height: 3px; -webkit-transition: none; transition: none; } md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate .md-container { -webkit-transition: none; transition: none; height: 3px; } md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-enter { -webkit-transition: opacity 0.15s linear; transition: opacity 0.15s linear; } md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-enter.ng-enter-active { opacity: 1; } md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-leave { -webkit-transition: opacity 0.15s linear; transition: opacity 0.15s linear; } md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-leave.ng-leave-active { opacity: 0; } md-autocomplete input:not(.md-input) { font-size: 14px; box-sizing: border-box; border: none; box-shadow: none; outline: none; background: transparent; width: 100%; padding: 0 15px; line-height: 40px; height: 40px; } md-autocomplete input:not(.md-input)::-ms-clear { display: none; } md-autocomplete .md-show-clear-button button { position: relative; line-height: 20px; text-align: center; width: 30px; height: 30px; cursor: pointer; border: none; border-radius: 50%; padding: 0; font-size: 12px; background: transparent; margin: auto 5px; } md-autocomplete .md-show-clear-button button:after { content: ''; position: absolute; top: -6px; right: -6px; bottom: -6px; left: -6px; border-radius: 50%; -webkit-transform: scale(0); transform: scale(0); opacity: 0; -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } md-autocomplete .md-show-clear-button button:focus { outline: none; } md-autocomplete .md-show-clear-button button:focus:after { -webkit-transform: scale(1); transform: scale(1); opacity: 1; } md-autocomplete .md-show-clear-button button md-icon { position: absolute; top: 50%; left: 50%; -webkit-transform: translate3d(-50%, -50%, 0) scale(0.9); transform: translate3d(-50%, -50%, 0) scale(0.9); } md-autocomplete .md-show-clear-button button md-icon path { stroke-width: 0; } md-autocomplete .md-show-clear-button button.ng-enter { -webkit-transform: scale(0); transform: scale(0); -webkit-transition: -webkit-transform 0.15s ease-out; transition: -webkit-transform 0.15s ease-out; transition: transform 0.15s ease-out; transition: transform 0.15s ease-out, -webkit-transform 0.15s ease-out; } md-autocomplete .md-show-clear-button button.ng-enter.ng-enter-active { -webkit-transform: scale(1); transform: scale(1); } md-autocomplete .md-show-clear-button button.ng-leave { -webkit-transition: -webkit-transform 0.15s ease-out; transition: -webkit-transform 0.15s ease-out; transition: transform 0.15s ease-out; transition: transform 0.15s ease-out, -webkit-transform 0.15s ease-out; } md-autocomplete .md-show-clear-button button.ng-leave.ng-leave-active { -webkit-transform: scale(0); transform: scale(0); } @media screen and (-ms-high-contrast: active) { md-autocomplete input { border: 1px solid #fff; } md-autocomplete li:focus { color: #fff; } } .md-virtual-repeat-container.md-autocomplete-suggestions-container { position: absolute; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25); z-index: 100; height: 100%; } .md-virtual-repeat-container.md-not-found { height: 48px; } .md-autocomplete-suggestions { margin: 0; list-style: none; padding: 0; } .md-autocomplete-suggestions li { font-size: 14px; overflow: hidden; padding: 0 15px; line-height: 48px; height: 48px; -webkit-transition: background 0.15s linear; transition: background 0.15s linear; margin: 0; white-space: nowrap; text-overflow: ellipsis; } .md-autocomplete-suggestions li:focus { outline: none; } .md-autocomplete-suggestions li:not(.md-not-found-wrapper) { cursor: pointer; } @media screen and (-ms-high-contrast: active) { md-autocomplete, .md-autocomplete-suggestions { border: 1px solid #fff; } } md-backdrop { -webkit-transition: opacity 450ms; transition: opacity 450ms; position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 50; } md-backdrop.md-menu-backdrop { position: fixed !important; z-index: 99; } md-backdrop.md-select-backdrop { z-index: 81; -webkit-transition-duration: 0; transition-duration: 0; } md-backdrop.md-dialog-backdrop { z-index: 79; } md-backdrop.md-bottom-sheet-backdrop { z-index: 69; } md-backdrop.md-sidenav-backdrop { z-index: 59; } md-backdrop.md-click-catcher { position: absolute; } md-backdrop.md-opaque { opacity: .48; } md-backdrop.md-opaque.ng-enter { opacity: 0; } md-backdrop.md-opaque.ng-enter.md-opaque.ng-enter-active { opacity: .48; } md-backdrop.md-opaque.ng-leave { opacity: .48; -webkit-transition: opacity 400ms; transition: opacity 400ms; } md-backdrop.md-opaque.ng-leave.md-opaque.ng-leave-active { opacity: 0; } md-bottom-sheet { position: absolute; left: 0; right: 0; bottom: 0; padding: 8px 16px 88px 16px; z-index: 70; border-top-width: 1px; border-top-style: solid; -webkit-transform: translate3d(0, 80px, 0); transform: translate3d(0, 80px, 0); -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transition-property: -webkit-transform; transition-property: -webkit-transform; transition-property: transform; transition-property: transform, -webkit-transform; } md-bottom-sheet.md-has-header { padding-top: 0; } md-bottom-sheet.ng-enter { opacity: 0; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } md-bottom-sheet.ng-enter-active { opacity: 1; display: block; -webkit-transform: translate3d(0, 80px, 0) !important; transform: translate3d(0, 80px, 0) !important; } md-bottom-sheet.ng-leave-active { -webkit-transform: translate3d(0, 100%, 0) !important; transform: translate3d(0, 100%, 0) !important; -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } md-bottom-sheet .md-subheader { background-color: transparent; font-family: Roboto, "Helvetica Neue", sans-serif; line-height: 56px; padding: 0; white-space: nowrap; } md-bottom-sheet md-inline-icon { display: inline-block; height: 24px; width: 24px; fill: #444; } md-bottom-sheet md-list-item { display: -webkit-box; display: -webkit-flex; display: flex; outline: none; } md-bottom-sheet md-list-item:hover { cursor: pointer; } md-bottom-sheet.md-list md-list-item { padding: 0; -webkit-box-align: center; -webkit-align-items: center; align-items: center; height: 48px; } md-bottom-sheet.md-grid { padding-left: 24px; padding-right: 24px; padding-top: 0; } md-bottom-sheet.md-grid md-list { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; -webkit-flex-wrap: wrap; flex-wrap: wrap; -webkit-transition: all 0.5s; transition: all 0.5s; -webkit-box-align: center; -webkit-align-items: center; align-items: center; } md-bottom-sheet.md-grid md-list-item { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; -webkit-box-align: center; -webkit-align-items: center; align-items: center; -webkit-transition: all 0.5s; transition: all 0.5s; height: 96px; margin-top: 8px; margin-bottom: 8px; /* Mixin for how many grid items to show per row */ } @media (max-width: 960px) { md-bottom-sheet.md-grid md-list-item { -webkit-box-flex: 1; -webkit-flex: 1 1 33.33333%; flex: 1 1 33.33333%; max-width: 33.33333%; } md-bottom-sheet.md-grid md-list-item:nth-of-type(3n + 1) { -webkit-box-align: start; -webkit-align-items: flex-start; align-items: flex-start; } md-bottom-sheet.md-grid md-list-item:nth-of-type(3n) { -webkit-box-align: end; -webkit-align-items: flex-end; align-items: flex-end; } } @media (min-width: 960px) and (max-width: 1279px) { md-bottom-sheet.md-grid md-list-item { -webkit-box-flex: 1; -webkit-flex: 1 1 25%; flex: 1 1 25%; max-width: 25%; } } @media (min-width: 1280px) and (max-width: 1919px) { md-bottom-sheet.md-grid md-list-item { -webkit-box-flex: 1; -webkit-flex: 1 1 16.66667%; flex: 1 1 16.66667%; max-width: 16.66667%; } } @media (min-width: 1920px) { md-bottom-sheet.md-grid md-list-item { -webkit-box-flex: 1; -webkit-flex: 1 1 14.28571%; flex: 1 1 14.28571%; max-width: 14.28571%; } } md-bottom-sheet.md-grid md-list-item::before { display: none; } md-bottom-sheet.md-grid md-list-item .md-list-item-content { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; -webkit-box-align: center; -webkit-align-items: center; align-items: center; width: 48px; padding-bottom: 16px; } md-bottom-sheet.md-grid md-list-item .md-grid-item-content { border: 1px solid transparent; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; -webkit-box-align: center; -webkit-align-items: center; align-items: center; width: 80px; } md-bottom-sheet.md-grid md-list-item .md-grid-text { font-weight: 400; line-height: 16px; font-size: 13px; margin: 0; white-space: nowrap; width: 64px; text-align: center; text-transform: none; padding-top: 8px; } @media screen and (-ms-high-contrast: active) { md-bottom-sheet { border: 1px solid #fff; } } button.md-button::-moz-focus-inner { border: 0; } .md-button { display: inline-block; position: relative; cursor: pointer; /** Alignment adjustments */ min-height: 36px; min-width: 88px; line-height: 36px; vertical-align: middle; -webkit-box-align: center; -webkit-align-items: center; align-items: center; text-align: center; border-radius: 2px; box-sizing: border-box; /* Reset default button appearance */ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; outline: none; border: 0; /** Custom styling for button */ padding: 0 6px; margin: 6px 8px; background: transparent; color: currentColor; white-space: nowrap; /* Uppercase text content */ text-transform: uppercase; font-weight: 500; font-size: 14px; font-style: inherit; font-variant: inherit; font-family: inherit; text-decoration: none; overflow: hidden; -webkit-transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } .md-dense > .md-button:not(.md-dense-disabled), .md-dense :not(.md-dense-disabled) .md-button:not(.md-dense-disabled) { min-height: 32px; } .md-dense > .md-button:not(.md-dense-disabled), .md-dense :not(.md-dense-disabled) .md-button:not(.md-dense-disabled) { line-height: 32px; } .md-dense > .md-button:not(.md-dense-disabled), .md-dense :not(.md-dense-disabled) .md-button:not(.md-dense-disabled) { font-size: 13px; } .md-button:focus { outline: none; } .md-button:hover, .md-button:focus { text-decoration: none; } .md-button.ng-hide, .md-button.ng-leave { -webkit-transition: none; transition: none; } .md-button.md-cornered { border-radius: 0; } .md-button.md-icon { padding: 0; background: none; } .md-button.md-raised:not([disabled]) { box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); } .md-button.md-icon-button { margin: 0 6px; height: 40px; min-width: 0; line-height: 24px; padding: 8px; width: 40px; border-radius: 50%; } .md-button.md-icon-button .md-ripple-container { border-radius: 50%; background-clip: padding-box; overflow: hidden; -webkit-mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC"); } .md-button.md-fab { z-index: 20; line-height: 56px; min-width: 0; width: 56px; height: 56px; vertical-align: middle; box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); border-radius: 50%; background-clip: padding-box; overflow: hidden; -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); -webkit-transition-property: background-color, box-shadow, -webkit-transform; transition-property: background-color, box-shadow, -webkit-transform; transition-property: background-color, box-shadow, transform; transition-property: background-color, box-shadow, transform, -webkit-transform; } .md-button.md-fab.md-fab-bottom-right { top: auto; right: 20px; bottom: 20px; left: auto; position: absolute; } .md-button.md-fab.md-fab-bottom-left { top: auto; right: auto; bottom: 20px; left: 20px; position: absolute; } .md-button.md-fab.md-fab-top-right { top: 20px; right: 20px; bottom: auto; left: auto; position: absolute; } .md-button.md-fab.md-fab-top-left { top: 20px; right: auto; bottom: auto; left: 20px; position: absolute; } .md-button.md-fab .md-ripple-container { border-radius: 50%; background-clip: padding-box; overflow: hidden; -webkit-mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC"); } .md-button.md-fab.md-mini { line-height: 40px; width: 40px; height: 40px; } .md-button.md-fab.ng-hide, .md-button.md-fab.ng-leave { -webkit-transition: none; transition: none; } .md-button:not([disabled]).md-raised.md-focused, .md-button:not([disabled]).md-fab.md-focused { box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); } .md-button:not([disabled]).md-raised:active, .md-button:not([disabled]).md-fab:active { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4); } .md-button .md-ripple-container { border-radius: 2px; background-clip: padding-box; overflow: hidden; -webkit-mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC"); } .md-button.md-icon-button md-icon, button.md-button.md-fab md-icon { display: block; } .md-toast-open-top .md-button.md-fab-top-left, .md-toast-open-top .md-button.md-fab-top-right { -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transform: translate3d(0, 42px, 0); transform: translate3d(0, 42px, 0); } .md-toast-open-top .md-button.md-fab-top-left:not([disabled]).md-focused, .md-toast-open-top .md-button.md-fab-top-left:not([disabled]):hover, .md-toast-open-top .md-button.md-fab-top-right:not([disabled]).md-focused, .md-toast-open-top .md-button.md-fab-top-right:not([disabled]):hover { -webkit-transform: translate3d(0, 41px, 0); transform: translate3d(0, 41px, 0); } .md-toast-open-bottom .md-button.md-fab-bottom-left, .md-toast-open-bottom .md-button.md-fab-bottom-right { -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transform: translate3d(0, -42px, 0); transform: translate3d(0, -42px, 0); } .md-toast-open-bottom .md-button.md-fab-bottom-left:not([disabled]).md-focused, .md-toast-open-bottom .md-button.md-fab-bottom-left:not([disabled]):hover, .md-toast-open-bottom .md-button.md-fab-bottom-right:not([disabled]).md-focused, .md-toast-open-bottom .md-button.md-fab-bottom-right:not([disabled]):hover { -webkit-transform: translate3d(0, -43px, 0); transform: translate3d(0, -43px, 0); } .md-button-group { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; width: 100%; } .md-button-group > .md-button { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: block; overflow: hidden; width: 0; border-width: 1px 0px 1px 1px; border-radius: 0; text-align: center; text-overflow: ellipsis; white-space: nowrap; } .md-button-group > .md-button:first-child { border-radius: 2px 0px 0px 2px; } .md-button-group > .md-button:last-child { border-right-width: 1px; border-radius: 0px 2px 2px 0px; } @media screen and (-ms-high-contrast: active) { .md-button.md-raised, .md-button.md-fab { border: 1px solid #fff; } } md-card { box-sizing: border-box; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; margin: 8px; box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12); } md-card md-card-header { padding: 16px; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-card md-card-header:first-child md-card-avatar { margin-right: 12px; } [dir=rtl] md-card md-card-header:first-child md-card-avatar { margin-right: auto; margin-left: 12px; } md-card md-card-header:last-child md-card-avatar { margin-left: 12px; } [dir=rtl] md-card md-card-header:last-child md-card-avatar { margin-left: auto; margin-right: 12px; } md-card md-card-header md-card-avatar { width: 40px; height: 40px; } md-card md-card-header md-card-avatar .md-user-avatar, md-card md-card-header md-card-avatar md-icon { border-radius: 50%; } md-card md-card-header md-card-avatar md-icon { padding: 8px; } md-card md-card-header md-card-avatar md-icon > svg { height: inherit; width: inherit; } md-card md-card-header md-card-avatar + md-card-header-text { max-height: 40px; } md-card md-card-header md-card-avatar + md-card-header-text .md-title { font-size: 14px; } md-card md-card-header md-card-header-text { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } md-card md-card-header md-card-header-text .md-subhead { font-size: 14px; } md-card > img, md-card > md-card-header img, md-card md-card-title-media img { box-sizing: border-box; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; width: 100%; height: auto; } md-card md-card-title { padding: 24px 16px 16px; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-card md-card-title + md-card-content { padding-top: 0; } md-card md-card-title md-card-title-text { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; display: -webkit-box; display: -webkit-flex; display: flex; } md-card md-card-title md-card-title-text .md-subhead { padding-top: 0; font-size: 14px; } md-card md-card-title md-card-title-text:only-child .md-subhead { padding-top: 12px; } md-card md-card-title md-card-title-media { margin-top: -8px; } md-card md-card-title md-card-title-media .md-media-sm { height: 80px; width: 80px; } md-card md-card-title md-card-title-media .md-media-md { height: 112px; width: 112px; } md-card md-card-title md-card-title-media .md-media-lg { height: 152px; width: 152px; } md-card md-card-content { display: block; padding: 16px; } md-card md-card-content > p:first-child { margin-top: 0; } md-card md-card-content > p:last-child { margin-bottom: 0; } md-card md-card-content .md-media-xl { height: 240px; width: 240px; } md-card .md-actions, md-card md-card-actions { margin: 8px; } md-card .md-actions.layout-column .md-button:not(.md-icon-button), md-card md-card-actions.layout-column .md-button:not(.md-icon-button) { margin: 2px 0; } md-card .md-actions.layout-column .md-button:not(.md-icon-button):first-of-type, md-card md-card-actions.layout-column .md-button:not(.md-icon-button):first-of-type { margin-top: 0; } md-card .md-actions.layout-column .md-button:not(.md-icon-button):last-of-type, md-card md-card-actions.layout-column .md-button:not(.md-icon-button):last-of-type { margin-bottom: 0; } md-card .md-actions.layout-column .md-button.md-icon-button, md-card md-card-actions.layout-column .md-button.md-icon-button { margin-top: 6px; margin-bottom: 6px; } md-card .md-actions md-card-icon-actions, md-card md-card-actions md-card-icon-actions { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; -webkit-box-pack: start; -webkit-justify-content: flex-start; justify-content: flex-start; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button), md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button) { margin: 0 4px; } md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type, md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type { margin-left: 0; } [dir=rtl] md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type { margin-left: auto; margin-right: 0; } md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type, md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type { margin-right: 0; } [dir=rtl] md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type { margin-right: auto; margin-left: 0; } md-card .md-actions:not(.layout-column) .md-button.md-icon-button, md-card md-card-actions:not(.layout-column) .md-button.md-icon-button { margin-left: 6px; margin-right: 6px; } md-card .md-actions:not(.layout-column) .md-button.md-icon-button:first-of-type, md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:first-of-type { margin-left: 12px; } [dir=rtl] md-card .md-actions:not(.layout-column) .md-button.md-icon-button:first-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:first-of-type { margin-left: auto; margin-right: 12px; } md-card .md-actions:not(.layout-column) .md-button.md-icon-button:last-of-type, md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:last-of-type { margin-right: 12px; } [dir=rtl] md-card .md-actions:not(.layout-column) .md-button.md-icon-button:last-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:last-of-type { margin-right: auto; margin-left: 12px; } md-card .md-actions:not(.layout-column) .md-button + md-card-icon-actions, md-card md-card-actions:not(.layout-column) .md-button + md-card-icon-actions { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; -webkit-box-pack: end; -webkit-justify-content: flex-end; justify-content: flex-end; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-card md-card-footer { margin-top: auto; padding: 16px; } @media screen and (-ms-high-contrast: active) { md-card { border: 1px solid #fff; } } .md-image-no-fill > img { width: auto; height: auto; } .md-contact-chips .md-chips md-chip { padding: 0 25px 0 0; } [dir=rtl] .md-contact-chips .md-chips md-chip { padding: 0 0 0 25px; } .md-contact-chips .md-chips md-chip .md-contact-avatar { float: left; } [dir=rtl] .md-contact-chips .md-chips md-chip .md-contact-avatar { float: right; } .md-contact-chips .md-chips md-chip .md-contact-avatar img { height: 32px; border-radius: 16px; } .md-contact-chips .md-chips md-chip .md-contact-name { display: inline-block; height: 32px; margin-left: 8px; } [dir=rtl] .md-contact-chips .md-chips md-chip .md-contact-name { margin-left: auto; margin-right: 8px; } .md-contact-suggestion { height: 56px; } .md-contact-suggestion img { height: 40px; border-radius: 20px; margin-top: 8px; } .md-contact-suggestion .md-contact-name { margin-left: 8px; width: 120px; } [dir=rtl] .md-contact-suggestion .md-contact-name { margin-left: auto; margin-right: 8px; } .md-contact-suggestion .md-contact-name, .md-contact-suggestion .md-contact-email { display: inline-block; overflow: hidden; text-overflow: ellipsis; } .md-contact-chips-suggestions li { height: 100%; } .md-chips { display: block; font-family: Roboto, "Helvetica Neue", sans-serif; font-size: 16px; padding: 0 0 8px 3px; vertical-align: middle; } .md-chips:after { content: ''; display: table; clear: both; } [dir=rtl] .md-chips { padding: 0 3px 8px 0; } .md-chips.md-readonly .md-chip-input-container { min-height: 32px; } .md-chips:not(.md-readonly) { cursor: text; } .md-chips.md-removable md-chip { padding-right: 22px; } [dir=rtl] .md-chips.md-removable md-chip { padding-right: 0; padding-left: 22px; } .md-chips.md-removable md-chip .md-chip-content { padding-right: 4px; } [dir=rtl] .md-chips.md-removable md-chip .md-chip-content { padding-right: 0; padding-left: 4px; } .md-chips md-chip { cursor: default; border-radius: 16px; display: block; height: 32px; line-height: 32px; margin: 8px 8px 0 0; padding: 0 12px 0 12px; float: left; box-sizing: border-box; max-width: 100%; position: relative; } [dir=rtl] .md-chips md-chip { margin: 8px 0 0 8px; } [dir=rtl] .md-chips md-chip { float: right; } .md-chips md-chip .md-chip-content { display: block; float: left; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; } [dir=rtl] .md-chips md-chip .md-chip-content { float: right; } .md-chips md-chip .md-chip-content:focus { outline: none; } .md-chips md-chip._md-chip-content-edit-is-enabled { -webkit-user-select: none; /* webkit (safari, chrome) browsers */ -moz-user-select: none; /* mozilla browsers */ -khtml-user-select: none; /* webkit (konqueror) browsers */ -ms-user-select: none; /* IE10+ */ } .md-chips md-chip .md-chip-remove-container { position: absolute; right: 0; line-height: 22px; } [dir=rtl] .md-chips md-chip .md-chip-remove-container { right: auto; left: 0; } .md-chips md-chip .md-chip-remove { text-align: center; width: 32px; height: 32px; min-width: 0; padding: 0; background: transparent; border: none; box-shadow: none; margin: 0; position: relative; } .md-chips md-chip .md-chip-remove md-icon { height: 18px; width: 18px; position: absolute; top: 50%; left: 50%; -webkit-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); } .md-chips .md-chip-input-container { display: block; line-height: 32px; margin: 8px 8px 0 0; padding: 0; float: left; } [dir=rtl] .md-chips .md-chip-input-container { margin: 8px 0 0 8px; } [dir=rtl] .md-chips .md-chip-input-container { float: right; } .md-chips .md-chip-input-container input:not([type]), .md-chips .md-chip-input-container input[type="email"], .md-chips .md-chip-input-container input[type="number"], .md-chips .md-chip-input-container input[type="tel"], .md-chips .md-chip-input-container input[type="url"], .md-chips .md-chip-input-container input[type="text"] { border: 0; height: 32px; line-height: 32px; padding: 0; } .md-chips .md-chip-input-container input:not([type]):focus, .md-chips .md-chip-input-container input[type="email"]:focus, .md-chips .md-chip-input-container input[type="number"]:focus, .md-chips .md-chip-input-container input[type="tel"]:focus, .md-chips .md-chip-input-container input[type="url"]:focus, .md-chips .md-chip-input-container input[type="text"]:focus { outline: none; } .md-chips .md-chip-input-container md-autocomplete, .md-chips .md-chip-input-container md-autocomplete-wrap { background: transparent; height: 32px; } .md-chips .md-chip-input-container md-autocomplete md-autocomplete-wrap { box-shadow: none; } .md-chips .md-chip-input-container md-autocomplete input { position: relative; } .md-chips .md-chip-input-container input { border: 0; height: 32px; line-height: 32px; padding: 0; } .md-chips .md-chip-input-container input:focus { outline: none; } .md-chips .md-chip-input-container md-autocomplete, .md-chips .md-chip-input-container md-autocomplete-wrap { height: 32px; } .md-chips .md-chip-input-container md-autocomplete { box-shadow: none; } .md-chips .md-chip-input-container md-autocomplete input { position: relative; } .md-chips .md-chip-input-container:not(:first-child) { margin: 8px 8px 0 0; } [dir=rtl] .md-chips .md-chip-input-container:not(:first-child) { margin: 8px 0 0 8px; } .md-chips .md-chip-input-container input { background: transparent; border-width: 0; } .md-chips md-autocomplete button { display: none; } @media screen and (-ms-high-contrast: active) { .md-chip-input-container, md-chip { border: 1px solid #fff; } .md-chip-input-container md-autocomplete { border: none; } } .md-inline-form md-checkbox { margin: 19px 0 18px; } md-checkbox { box-sizing: border-box; display: inline-block; margin-bottom: 16px; white-space: nowrap; cursor: pointer; outline: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: relative; min-width: 20px; min-height: 20px; margin-left: 0; margin-right: 16px; } [dir=rtl] md-checkbox { margin-left: 16px; } [dir=rtl] md-checkbox { margin-right: 0; } md-checkbox:last-of-type { margin-left: 0; margin-right: 0; } md-checkbox.md-focused:not([disabled]) .md-container:before { left: -8px; top: -8px; right: -8px; bottom: -8px; } md-checkbox.md-focused:not([disabled]):not(.md-checked) .md-container:before { background-color: rgba(0, 0, 0, 0.12); } md-checkbox.md-align-top-left > div.md-container { top: 12px; } md-checkbox .md-container { position: absolute; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); box-sizing: border-box; display: inline-block; width: 20px; height: 20px; left: 0; right: auto; } [dir=rtl] md-checkbox .md-container { left: auto; } [dir=rtl] md-checkbox .md-container { right: 0; } md-checkbox .md-container:before { box-sizing: border-box; background-color: transparent; border-radius: 50%; content: ''; position: absolute; display: block; height: auto; left: 0; top: 0; right: 0; bottom: 0; -webkit-transition: all 0.5s; transition: all 0.5s; width: auto; } md-checkbox .md-container:after { box-sizing: border-box; content: ''; position: absolute; top: -10px; right: -10px; bottom: -10px; left: -10px; } md-checkbox .md-container .md-ripple-container { position: absolute; display: block; width: auto; height: auto; left: -15px; top: -15px; right: -15px; bottom: -15px; } md-checkbox .md-icon { box-sizing: border-box; -webkit-transition: 240ms; transition: 240ms; position: absolute; top: 0; left: 0; width: 20px; height: 20px; border-width: 2px; border-style: solid; border-radius: 2px; } md-checkbox.md-checked .md-icon { border-color: transparent; } md-checkbox.md-checked .md-icon:after { box-sizing: border-box; -webkit-transform: rotate(45deg); transform: rotate(45deg); position: absolute; left: 4.66667px; top: 0.22222px; display: table; width: 6.66667px; height: 13.33333px; border-width: 2px; border-style: solid; border-top: 0; border-left: 0; content: ''; } md-checkbox[disabled] { cursor: default; } md-checkbox.md-indeterminate .md-icon:after { box-sizing: border-box; position: absolute; top: 50%; left: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); display: table; width: 12px; height: 2px; border-width: 2px; border-style: solid; border-top: 0; border-left: 0; content: ''; } md-checkbox .md-label { box-sizing: border-box; position: relative; display: inline-block; vertical-align: middle; white-space: normal; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; margin-left: 30px; margin-right: 0; } [dir=rtl] md-checkbox .md-label { margin-left: 0; } [dir=rtl] md-checkbox .md-label { margin-right: 30px; } md-content { display: block; position: relative; overflow: auto; -webkit-overflow-scrolling: touch; } md-content[md-scroll-y] { overflow-y: auto; overflow-x: hidden; } md-content[md-scroll-x] { overflow-x: auto; overflow-y: hidden; } @media print { md-content { overflow: visible !important; } } /** Styles for mdCalendar. */ md-calendar { font-size: 13px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .md-calendar-scroll-mask { display: inline-block; overflow: hidden; height: 308px; } .md-calendar-scroll-mask .md-virtual-repeat-scroller { overflow-y: scroll; -webkit-overflow-scrolling: touch; } .md-calendar-scroll-mask .md-virtual-repeat-scroller::-webkit-scrollbar { display: none; } .md-calendar-scroll-mask .md-virtual-repeat-offsetter { width: 100%; } .md-calendar-scroll-container { box-shadow: inset -3px 3px 6px rgba(0, 0, 0, 0.2); display: inline-block; height: 308px; width: 346px; } .md-calendar-date { height: 44px; width: 44px; text-align: center; padding: 0; border: none; box-sizing: content-box; } .md-calendar-date:first-child { padding-left: 16px; } [dir=rtl] .md-calendar-date:first-child { padding-left: 0; padding-right: 16px; } .md-calendar-date:last-child { padding-right: 16px; } [dir=rtl] .md-calendar-date:last-child { padding-right: 0; padding-left: 16px; } .md-calendar-date.md-calendar-date-disabled { cursor: default; } .md-calendar-date-selection-indicator { -webkit-transition: background-color, color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: background-color, color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); border-radius: 50%; display: inline-block; width: 40px; height: 40px; line-height: 40px; } .md-calendar-date:not(.md-disabled) .md-calendar-date-selection-indicator { cursor: pointer; } .md-calendar-month-label { height: 44px; font-size: 14px; font-weight: 500; padding: 0 0 0 24px; } [dir=rtl] .md-calendar-month-label { padding: 0 24px 0 0; } md-calendar-month .md-calendar-month-label:not(.md-calendar-month-label-disabled) { cursor: pointer; } .md-calendar-month-label md-icon { -webkit-transform: rotate(180deg); transform: rotate(180deg); } [dir=rtl] .md-calendar-month-label md-icon { -webkit-transform: none; transform: none; } .md-calendar-month-label span { vertical-align: middle; } .md-calendar-day-header { table-layout: fixed; border-spacing: 0; border-collapse: collapse; } .md-calendar-day-header th { height: 40px; width: 44px; text-align: center; padding: 0; border: none; box-sizing: content-box; font-weight: normal; } .md-calendar-day-header th:first-child { padding-left: 16px; } [dir=rtl] .md-calendar-day-header th:first-child { padding-left: 0; padding-right: 16px; } .md-calendar-day-header th:last-child { padding-right: 16px; } [dir=rtl] .md-calendar-day-header th:last-child { padding-right: 0; padding-left: 16px; } .md-calendar { table-layout: fixed; border-spacing: 0; border-collapse: collapse; } .md-calendar tr:last-child td { border-bottom-width: 1px; border-bottom-style: solid; } .md-calendar:first-child { border-top: 1px solid transparent; } .md-calendar tbody, .md-calendar td, .md-calendar tr { vertical-align: middle; box-sizing: content-box; } /** Styles for mdDatepicker. */ md-datepicker { white-space: nowrap; overflow: hidden; vertical-align: middle; } .md-inline-form md-datepicker { margin-top: 12px; } .md-datepicker-button { display: inline-block; box-sizing: border-box; background: none; vertical-align: middle; position: relative; } .md-datepicker-button:before { top: 0; left: 0; bottom: 0; right: 0; position: absolute; content: ''; speak: none; } .md-datepicker-input { font-size: 14px; box-sizing: border-box; border: none; box-shadow: none; outline: none; background: transparent; min-width: 120px; max-width: 328px; padding: 0 0 5px; } .md-datepicker-input::-ms-clear { display: none; } ._md-datepicker-floating-label > md-datepicker { overflow: visible; } ._md-datepicker-floating-label > md-datepicker .md-datepicker-input-container { border: none; } ._md-datepicker-floating-label > md-datepicker .md-datepicker-button { float: left; margin-top: -12px; top: 9.5px; } [dir=rtl] ._md-datepicker-floating-label > md-datepicker .md-datepicker-button { float: right; } ._md-datepicker-floating-label .md-input { float: none; } ._md-datepicker-floating-label._md-datepicker-has-calendar-icon > label:not(.md-no-float):not(.md-container-ignore) { right: 18px; left: auto; width: calc(100% - 84px); } [dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon > label:not(.md-no-float):not(.md-container-ignore) { right: auto; } [dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon > label:not(.md-no-float):not(.md-container-ignore) { left: 18px; } ._md-datepicker-floating-label._md-datepicker-has-calendar-icon .md-input-message-animation { margin-left: 64px; } [dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon .md-input-message-animation { margin-left: auto; margin-right: 64px; } ._md-datepicker-has-triangle-icon { padding-right: 18px; margin-right: -18px; } [dir=rtl] ._md-datepicker-has-triangle-icon { padding-right: 0; padding-left: 18px; } [dir=rtl] ._md-datepicker-has-triangle-icon { margin-right: auto; margin-left: -18px; } .md-datepicker-input-container { position: relative; border-bottom-width: 1px; border-bottom-style: solid; display: inline-block; width: auto; } .md-icon-button + .md-datepicker-input-container { margin-left: 12px; } [dir=rtl] .md-icon-button + .md-datepicker-input-container { margin-left: auto; margin-right: 12px; } .md-datepicker-input-container.md-datepicker-focused { border-bottom-width: 2px; } .md-datepicker-is-showing .md-scroll-mask { z-index: 99; } .md-datepicker-calendar-pane { position: absolute; top: 0; left: -100%; z-index: 100; border-width: 1px; border-style: solid; background: transparent; -webkit-transform: scale(0); transform: scale(0); -webkit-transform-origin: 0 0; transform-origin: 0 0; -webkit-transition: -webkit-transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); transition: -webkit-transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); transition: transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); transition: transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), -webkit-transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); } .md-datepicker-calendar-pane.md-pane-open { -webkit-transform: scale(1); transform: scale(1); } .md-datepicker-input-mask { height: 40px; width: 340px; position: relative; overflow: hidden; background: transparent; pointer-events: none; cursor: text; } .md-datepicker-calendar { opacity: 0; -webkit-transition: opacity 0.2s cubic-bezier(0.5, 0, 0.25, 1); transition: opacity 0.2s cubic-bezier(0.5, 0, 0.25, 1); } .md-pane-open .md-datepicker-calendar { opacity: 1; } .md-datepicker-calendar md-calendar:focus { outline: none; } .md-datepicker-expand-triangle { position: absolute; top: 50%; left: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid; } .md-datepicker-triangle-button { position: absolute; right: 0; bottom: -2.5px; -webkit-transform: translateX(45%); transform: translateX(45%); } [dir=rtl] .md-datepicker-triangle-button { right: auto; left: 0; } [dir=rtl] .md-datepicker-triangle-button { -webkit-transform: translateX(-45%); transform: translateX(-45%); } .md-datepicker-triangle-button.md-button.md-icon-button { height: 36px; width: 36px; position: absolute; padding: 8px; } md-datepicker[disabled] .md-datepicker-input-container { border-bottom-color: transparent; } md-datepicker[disabled] .md-datepicker-triangle-button { display: none; } .md-datepicker-open { overflow: hidden; } .md-datepicker-open .md-datepicker-input-container, .md-datepicker-open input.md-input { border-bottom-color: transparent; } .md-datepicker-open .md-datepicker-triangle-button, .md-datepicker-open.md-input-has-value > label, .md-datepicker-open.md-input-has-placeholder > label { display: none; } .md-datepicker-pos-adjusted .md-datepicker-input-mask { display: none; } .md-datepicker-calendar-pane .md-calendar { -webkit-transform: translateY(-85px); transform: translateY(-85px); -webkit-transition: -webkit-transform 0.65s cubic-bezier(0.25, 0.8, 0.25, 1); transition: -webkit-transform 0.65s cubic-bezier(0.25, 0.8, 0.25, 1); transition: transform 0.65s cubic-bezier(0.25, 0.8, 0.25, 1); transition: transform 0.65s cubic-bezier(0.25, 0.8, 0.25, 1), -webkit-transform 0.65s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transition-delay: 0.125s; transition-delay: 0.125s; } .md-datepicker-calendar-pane.md-pane-open .md-calendar { -webkit-transform: translateY(0); transform: translateY(0); } .md-dialog-is-showing { max-height: 100%; } .md-dialog-container { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: center; -webkit-justify-content: center; justify-content: center; -webkit-box-align: center; -webkit-align-items: center; align-items: center; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 80; overflow: hidden; } md-dialog { opacity: 0; min-width: 240px; max-width: 80%; max-height: 80%; position: relative; overflow: auto; box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 13px 19px 2px rgba(0, 0, 0, 0.14), 0px 5px 24px 4px rgba(0, 0, 0, 0.12); display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } md-dialog.md-transition-in { opacity: 1; -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1); } md-dialog.md-transition-out { opacity: 0; -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transform: translate(0, 100%) scale(0.2); transform: translate(0, 100%) scale(0.2); } md-dialog > form { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; overflow: auto; } md-dialog .md-dialog-content { padding: 24px; } md-dialog md-dialog-content { -webkit-box-ordinal-group: 2; -webkit-order: 1; order: 1; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; overflow: auto; -webkit-overflow-scrolling: touch; } md-dialog md-dialog-content:not([layout=row]) > *:first-child:not(.md-subheader) { margin-top: 0; } md-dialog md-dialog-content:focus { outline: none; } md-dialog md-dialog-content .md-subheader { margin: 0; } md-dialog md-dialog-content .md-dialog-content-body { width: 100%; } md-dialog md-dialog-content .md-prompt-input-container { width: 100%; box-sizing: border-box; } md-dialog .md-actions, md-dialog md-dialog-actions { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-ordinal-group: 3; -webkit-order: 2; order: 2; box-sizing: border-box; -webkit-box-align: center; -webkit-align-items: center; align-items: center; -webkit-box-pack: end; -webkit-justify-content: flex-end; justify-content: flex-end; margin-bottom: 0; padding-right: 8px; padding-left: 16px; min-height: 52px; overflow: hidden; } [dir=rtl] md-dialog .md-actions, [dir=rtl] md-dialog md-dialog-actions { padding-right: 16px; } [dir=rtl] md-dialog .md-actions, [dir=rtl] md-dialog md-dialog-actions { padding-left: 8px; } md-dialog .md-actions .md-button, md-dialog md-dialog-actions .md-button { margin-bottom: 8px; margin-left: 8px; margin-right: 0; margin-top: 8px; } [dir=rtl] md-dialog .md-actions .md-button, [dir=rtl] md-dialog md-dialog-actions .md-button { margin-left: 0; } [dir=rtl] md-dialog .md-actions .md-button, [dir=rtl] md-dialog md-dialog-actions .md-button { margin-right: 8px; } md-dialog.md-content-overflow .md-actions, md-dialog.md-content-overflow md-dialog-actions { border-top-width: 1px; border-top-style: solid; } @media screen and (-ms-high-contrast: active) { md-dialog { border: 1px solid #fff; } } @media (max-width: 959px) { md-dialog.md-dialog-fullscreen { min-height: 100%; min-width: 100%; border-radius: 0; } } md-divider { display: block; border-top-width: 1px; border-top-style: solid; margin: 0; } md-divider[md-inset] { margin-left: 80px; } [dir=rtl] md-divider[md-inset] { margin-left: auto; margin-right: 80px; } .layout-row > md-divider, .layout-xs-row > md-divider, .layout-gt-xs-row > md-divider, .layout-sm-row > md-divider, .layout-gt-sm-row > md-divider, .layout-md-row > md-divider, .layout-gt-md-row > md-divider, .layout-lg-row > md-divider, .layout-gt-lg-row > md-divider, .layout-xl-row > md-divider { border-top-width: 0; border-right-width: 1px; border-right-style: solid; } md-fab-speed-dial { position: relative; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; z-index: 20; /* * Hide some graphics glitches if switching animation types */ /* * Handle the animations */ } md-fab-speed-dial.md-fab-bottom-right { top: auto; right: 20px; bottom: 20px; left: auto; position: absolute; } md-fab-speed-dial.md-fab-bottom-left { top: auto; right: auto; bottom: 20px; left: 20px; position: absolute; } md-fab-speed-dial.md-fab-top-right { top: 20px; right: 20px; bottom: auto; left: auto; position: absolute; } md-fab-speed-dial.md-fab-top-left { top: 20px; right: auto; bottom: auto; left: 20px; position: absolute; } md-fab-speed-dial:not(.md-hover-full) { pointer-events: none; } md-fab-speed-dial:not(.md-hover-full) md-fab-trigger, md-fab-speed-dial:not(.md-hover-full) .md-fab-action-item { pointer-events: auto; } md-fab-speed-dial:not(.md-hover-full).md-is-open { pointer-events: auto; } md-fab-speed-dial ._md-css-variables { z-index: 20; } md-fab-speed-dial.md-is-open .md-fab-action-item { -webkit-box-align: center; -webkit-align-items: center; align-items: center; } md-fab-speed-dial md-fab-actions { display: -webkit-box; display: -webkit-flex; display: flex; height: auto; } md-fab-speed-dial md-fab-actions .md-fab-action-item { -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } md-fab-speed-dial.md-down { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } md-fab-speed-dial.md-down md-fab-trigger { -webkit-box-ordinal-group: 2; -webkit-order: 1; order: 1; } md-fab-speed-dial.md-down md-fab-actions { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; -webkit-box-ordinal-group: 3; -webkit-order: 2; order: 2; } md-fab-speed-dial.md-up { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } md-fab-speed-dial.md-up md-fab-trigger { -webkit-box-ordinal-group: 3; -webkit-order: 2; order: 2; } md-fab-speed-dial.md-up md-fab-actions { -webkit-box-orient: vertical; -webkit-box-direction: reverse; -webkit-flex-direction: column-reverse; flex-direction: column-reverse; -webkit-box-ordinal-group: 2; -webkit-order: 1; order: 1; } md-fab-speed-dial.md-left { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-fab-speed-dial.md-left md-fab-trigger { -webkit-box-ordinal-group: 3; -webkit-order: 2; order: 2; } md-fab-speed-dial.md-left md-fab-actions { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -webkit-flex-direction: row-reverse; flex-direction: row-reverse; -webkit-box-ordinal-group: 2; -webkit-order: 1; order: 1; } md-fab-speed-dial.md-left md-fab-actions .md-fab-action-item { -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } md-fab-speed-dial.md-right { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-fab-speed-dial.md-right md-fab-trigger { -webkit-box-ordinal-group: 2; -webkit-order: 1; order: 1; } md-fab-speed-dial.md-right md-fab-actions { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; -webkit-box-ordinal-group: 3; -webkit-order: 2; order: 2; } md-fab-speed-dial.md-right md-fab-actions .md-fab-action-item { -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } md-fab-speed-dial.md-fling-remove .md-fab-action-item > *, md-fab-speed-dial.md-scale-remove .md-fab-action-item > * { visibility: hidden; } md-fab-speed-dial.md-fling .md-fab-action-item { opacity: 1; } md-fab-speed-dial.md-fling.md-animations-waiting .md-fab-action-item { opacity: 0; -webkit-transition-duration: 0s; transition-duration: 0s; } md-fab-speed-dial.md-scale .md-fab-action-item { -webkit-transform: scale(0); transform: scale(0); -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); -webkit-transition-duration: 0.14286s; transition-duration: 0.14286s; } md-fab-toolbar { display: block; /* * Closed styling */ /* * Hover styling */ } md-fab-toolbar.md-fab-bottom-right { top: auto; right: 20px; bottom: 20px; left: auto; position: absolute; } md-fab-toolbar.md-fab-bottom-left { top: auto; right: auto; bottom: 20px; left: 20px; position: absolute; } md-fab-toolbar.md-fab-top-right { top: 20px; right: 20px; bottom: auto; left: auto; position: absolute; } md-fab-toolbar.md-fab-top-left { top: 20px; right: auto; bottom: auto; left: 20px; position: absolute; } md-fab-toolbar .md-fab-toolbar-wrapper { display: block; position: relative; overflow: hidden; height: 68px; } md-fab-toolbar md-fab-trigger { position: absolute; z-index: 20; } md-fab-toolbar md-fab-trigger button { overflow: visible !important; } md-fab-toolbar md-fab-trigger .md-fab-toolbar-background { display: block; position: absolute; z-index: 21; opacity: 1; -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } md-fab-toolbar md-fab-trigger md-icon { position: relative; z-index: 22; opacity: 1; -webkit-transition: all 200ms ease-in; transition: all 200ms ease-in; } md-fab-toolbar.md-left md-fab-trigger { right: 0; } [dir=rtl] md-fab-toolbar.md-left md-fab-trigger { right: auto; left: 0; } md-fab-toolbar.md-left .md-toolbar-tools { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -webkit-flex-direction: row-reverse; flex-direction: row-reverse; } md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { margin-right: 0.6rem; } [dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { margin-right: auto; margin-left: 0.6rem; } md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { margin-left: -0.8rem; } [dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { margin-left: auto; margin-right: -0.8rem; } md-fab-toolbar.md-left .md-toolbar-tools > .md-button:last-child { margin-right: 8px; } [dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools > .md-button:last-child { margin-right: auto; margin-left: 8px; } md-fab-toolbar.md-right md-fab-trigger { left: 0; } [dir=rtl] md-fab-toolbar.md-right md-fab-trigger { left: auto; right: 0; } md-fab-toolbar.md-right .md-toolbar-tools { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; } md-fab-toolbar md-toolbar { background-color: transparent !important; pointer-events: none; z-index: 23; } md-fab-toolbar md-toolbar .md-toolbar-tools { padding: 0 20px; margin-top: 3px; } md-fab-toolbar md-toolbar .md-fab-action-item { opacity: 0; -webkit-transform: scale(0); transform: scale(0); -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); -webkit-transition-duration: 0.15s; transition-duration: 0.15s; } md-fab-toolbar.md-is-open md-fab-trigger > button { box-shadow: none; } md-fab-toolbar.md-is-open md-fab-trigger > button md-icon { opacity: 0; } md-fab-toolbar.md-is-open .md-fab-action-item { opacity: 1; -webkit-transform: scale(1); transform: scale(1); } md-icon { margin: auto; background-repeat: no-repeat no-repeat; display: inline-block; vertical-align: middle; fill: currentColor; height: 24px; width: 24px; min-height: 24px; min-width: 24px; } md-icon svg { pointer-events: none; display: block; } md-icon[md-font-icon] { line-height: 24px; width: auto; } md-grid-list { box-sizing: border-box; display: block; position: relative; } md-grid-list md-grid-tile, md-grid-list md-grid-tile > figure, md-grid-list md-grid-tile-header, md-grid-list md-grid-tile-footer { box-sizing: border-box; } md-grid-list md-grid-tile { display: block; position: absolute; } md-grid-list md-grid-tile figure { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; -webkit-box-pack: center; -webkit-justify-content: center; justify-content: center; height: 100%; position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: 0; margin: 0; } md-grid-list md-grid-tile md-grid-tile-header, md-grid-list md-grid-tile md-grid-tile-footer { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; -webkit-box-align: center; -webkit-align-items: center; align-items: center; height: 48px; color: #fff; background: rgba(0, 0, 0, 0.18); overflow: hidden; position: absolute; left: 0; right: 0; } md-grid-list md-grid-tile md-grid-tile-header h3, md-grid-list md-grid-tile md-grid-tile-header h4, md-grid-list md-grid-tile md-grid-tile-footer h3, md-grid-list md-grid-tile md-grid-tile-footer h4 { font-weight: 400; margin: 0 0 0 16px; } md-grid-list md-grid-tile md-grid-tile-header h3, md-grid-list md-grid-tile md-grid-tile-footer h3 { font-size: 14px; } md-grid-list md-grid-tile md-grid-tile-header h4, md-grid-list md-grid-tile md-grid-tile-footer h4 { font-size: 12px; } md-grid-list md-grid-tile md-grid-tile-header { top: 0; } md-grid-list md-grid-tile md-grid-tile-footer { bottom: 0; } @media screen and (-ms-high-contrast: active) { md-grid-tile { border: 1px solid #fff; } md-grid-tile-footer { border-top: 1px solid #fff; } } md-input-container { display: inline-block; position: relative; padding: 2px; margin: 18px 0; vertical-align: middle; /* * The .md-input class is added to the input/textarea */ } md-input-container:after { content: ''; display: table; clear: both; } md-input-container.md-block { display: block; } md-input-container .md-errors-spacer { float: right; min-height: 24px; min-width: 1px; } [dir=rtl] md-input-container .md-errors-spacer { float: left; } md-input-container > md-icon { position: absolute; top: 8px; left: 2px; right: auto; } [dir=rtl] md-input-container > md-icon { left: auto; } [dir=rtl] md-input-container > md-icon { right: 2px; } md-input-container textarea, md-input-container input[type="text"], md-input-container input[type="password"], md-input-container input[type="datetime"], md-input-container input[type="datetime-local"], md-input-container input[type="date"], md-input-container input[type="month"], md-input-container input[type="time"], md-input-container input[type="week"], md-input-container input[type="number"], md-input-container input[type="email"], md-input-container input[type="url"], md-input-container input[type="search"], md-input-container input[type="tel"], md-input-container input[type="color"] { /* remove default appearance from all input/textarea */ -moz-appearance: none; -webkit-appearance: none; } md-input-container input[type="date"], md-input-container input[type="datetime-local"], md-input-container input[type="month"], md-input-container input[type="time"], md-input-container input[type="week"] { min-height: 26px; } md-input-container textarea { resize: none; overflow: hidden; } md-input-container textarea.md-input { min-height: 26px; -ms-flex-preferred-size: auto; } md-input-container textarea[md-no-autogrow] { height: auto; overflow: auto; } md-input-container label:not(.md-container-ignore) { position: absolute; bottom: 100%; left: 0; right: auto; } [dir=rtl] md-input-container label:not(.md-container-ignore) { left: auto; } [dir=rtl] md-input-container label:not(.md-container-ignore) { right: 0; } md-input-container label:not(.md-container-ignore).md-required:after { content: ' *'; font-size: 13px; vertical-align: top; } md-input-container label:not(.md-no-float):not(.md-container-ignore), md-input-container .md-placeholder { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; -webkit-box-ordinal-group: 2; -webkit-order: 1; order: 1; pointer-events: none; -webkit-font-smoothing: antialiased; padding-left: 3px; padding-right: 0; z-index: 1; -webkit-transform: translate3d(0, 28px, 0) scale(1); transform: translate3d(0, 28px, 0) scale(1); -webkit-transition: -webkit-transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: -webkit-transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), -webkit-transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); max-width: 100%; -webkit-transform-origin: left top; transform-origin: left top; } [dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore), [dir=rtl] md-input-container .md-placeholder { padding-left: 0; } [dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore), [dir=rtl] md-input-container .md-placeholder { padding-right: 3px; } [dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore), [dir=rtl] md-input-container .md-placeholder { -webkit-transform-origin: right top; transform-origin: right top; } md-input-container .md-placeholder { position: absolute; top: 0; opacity: 0; -webkit-transition-property: opacity, -webkit-transform; transition-property: opacity, -webkit-transform; transition-property: opacity, transform; transition-property: opacity, transform, -webkit-transform; -webkit-transform: translate3d(0, 30px, 0); transform: translate3d(0, 30px, 0); } md-input-container.md-input-focused .md-placeholder { opacity: 1; -webkit-transform: translate3d(0, 24px, 0); transform: translate3d(0, 24px, 0); } md-input-container.md-input-has-value .md-placeholder { -webkit-transition: none; transition: none; opacity: 0; } md-input-container:not(.md-input-has-value) input:not(:focus), md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-ampm-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-day-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-hour-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-millisecond-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-minute-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-month-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-second-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-week-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-year-field, md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-text { color: transparent; } md-input-container .md-input { -webkit-box-ordinal-group: 3; -webkit-order: 2; order: 2; display: block; margin-top: 0; background: none; padding-top: 2px; padding-bottom: 1px; padding-left: 2px; padding-right: 2px; border-width: 0 0 1px 0; line-height: 26px; height: 30px; -ms-flex-preferred-size: 26px; border-radius: 0; border-style: solid; width: 100%; box-sizing: border-box; float: left; } [dir=rtl] md-input-container .md-input { float: right; } md-input-container .md-input:focus { outline: none; } md-input-container .md-input:invalid { outline: none; box-shadow: none; } md-input-container .md-input.md-no-flex { -webkit-box-flex: 0 !important; -webkit-flex: none !important; flex: none !important; } md-input-container .md-char-counter { text-align: right; padding-right: 2px; padding-left: 0; } [dir=rtl] md-input-container .md-char-counter { text-align: left; } [dir=rtl] md-input-container .md-char-counter { padding-right: 0; } [dir=rtl] md-input-container .md-char-counter { padding-left: 2px; } md-input-container .md-input-messages-animation { position: relative; -webkit-box-ordinal-group: 5; -webkit-order: 4; order: 4; overflow: hidden; clear: left; } [dir=rtl] md-input-container .md-input-messages-animation { clear: right; } md-input-container .md-input-message-animation, md-input-container .md-char-counter { font-size: 12px; line-height: 14px; overflow: hidden; -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); opacity: 1; margin-top: 0; padding-top: 5px; } md-input-container .md-input-message-animation:not(.md-char-counter), md-input-container .md-char-counter:not(.md-char-counter) { padding-right: 5px; padding-left: 0; } [dir=rtl] md-input-container .md-input-message-animation:not(.md-char-counter), [dir=rtl] md-input-container .md-char-counter:not(.md-char-counter) { padding-right: 0; } [dir=rtl] md-input-container .md-input-message-animation:not(.md-char-counter), [dir=rtl] md-input-container .md-char-counter:not(.md-char-counter) { padding-left: 5px; } md-input-container:not(.md-input-invalid) .md-auto-hide .md-input-message-animation { opacity: 0; margin-top: -100px; } md-input-container .md-input-message-animation.ng-enter-prepare { opacity: 0; margin-top: -100px; } md-input-container .md-input-message-animation.ng-enter:not(.ng-enter-active) { opacity: 0; margin-top: -100px; } md-input-container.md-input-focused label:not(.md-no-float), md-input-container.md-input-has-placeholder label:not(.md-no-float), md-input-container.md-input-has-value label:not(.md-no-float) { -webkit-transform: translate3d(0, 6px, 0) scale(0.75); transform: translate3d(0, 6px, 0) scale(0.75); -webkit-transition: width cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s, -webkit-transform cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s; transition: width cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s, -webkit-transform cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s; transition: transform cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s, width cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s; transition: transform cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s, width cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s, -webkit-transform cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s; } md-input-container.md-input-has-value label { -webkit-transition: none; transition: none; } md-input-container.md-input-focused .md-input, md-input-container .md-input.ng-invalid.ng-dirty, md-input-container.md-input-resized .md-input { padding-bottom: 0; border-width: 0 0 2px 0; } md-input-container .md-input[disabled], [disabled] md-input-container .md-input { background-position: bottom -1px left 0; background-size: 4px 1px; background-repeat: repeat-x; } md-input-container.md-icon-float { -webkit-transition: margin-top 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: margin-top 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } md-input-container.md-icon-float > label { pointer-events: none; position: absolute; } md-input-container.md-icon-float > md-icon { top: 8px; left: 2px; right: auto; } [dir=rtl] md-input-container.md-icon-float > md-icon { left: auto; } [dir=rtl] md-input-container.md-icon-float > md-icon { right: 2px; } md-input-container.md-icon-left > label:not(.md-no-float):not(.md-container-ignore), md-input-container.md-icon-left > label .md-placeholder, md-input-container.md-icon-right > label:not(.md-no-float):not(.md-container-ignore), md-input-container.md-icon-right > label .md-placeholder { width: calc(100% - 36px - 18px); } md-input-container.md-icon-left { padding-left: 36px; padding-right: 0; } [dir=rtl] md-input-container.md-icon-left { padding-left: 0; } [dir=rtl] md-input-container.md-icon-left { padding-right: 36px; } md-input-container.md-icon-left > label { left: 36px; right: auto; } [dir=rtl] md-input-container.md-icon-left > label { left: auto; } [dir=rtl] md-input-container.md-icon-left > label { right: 36px; } md-input-container.md-icon-right { padding-left: 0; padding-right: 36px; } [dir=rtl] md-input-container.md-icon-right { padding-left: 36px; } [dir=rtl] md-input-container.md-icon-right { padding-right: 0; } md-input-container.md-icon-right > md-icon:last-of-type { margin: 0; right: 2px; left: auto; } [dir=rtl] md-input-container.md-icon-right > md-icon:last-of-type { right: auto; } [dir=rtl] md-input-container.md-icon-right > md-icon:last-of-type { left: 2px; } md-input-container.md-icon-left.md-icon-right { padding-left: 36px; padding-right: 36px; } md-input-container.md-icon-left.md-icon-right > label:not(.md-no-float):not(.md-container-ignore), md-input-container.md-icon-left.md-icon-right > label .md-placeholder { width: calc(100% - (36px * 2)); } .md-resize-wrapper { position: relative; } .md-resize-wrapper:after { content: ''; display: table; clear: both; } .md-resize-handle { position: absolute; bottom: -5px; left: 0; height: 10px; background: transparent; width: 100%; cursor: ns-resize; } @media screen and (-ms-high-contrast: active) { md-input-container.md-default-theme > md-icon { fill: #fff; } } md-list { display: block; padding: 8px 0px 8px 0px; } md-list .md-subheader { font-size: 14px; font-weight: 500; letter-spacing: 0.010em; line-height: 1.2em; } md-list.md-dense md-list-item, md-list.md-dense md-list-item .md-list-item-inner { min-height: 48px; } md-list.md-dense md-list-item::before, md-list.md-dense md-list-item .md-list-item-inner::before { content: ''; min-height: 48px; visibility: hidden; display: inline-block; } md-list.md-dense md-list-item md-icon:first-child, md-list.md-dense md-list-item .md-list-item-inner md-icon:first-child { width: 20px; height: 20px; } md-list.md-dense md-list-item > md-icon:first-child:not(.md-avatar-icon), md-list.md-dense md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { margin-right: 36px; } [dir=rtl] md-list.md-dense md-list-item > md-icon:first-child:not(.md-avatar-icon), [dir=rtl] md-list.md-dense md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { margin-right: auto; margin-left: 36px; } md-list.md-dense md-list-item .md-avatar, md-list.md-dense md-list-item .md-avatar-icon, md-list.md-dense md-list-item .md-list-item-inner .md-avatar, md-list.md-dense md-list-item .md-list-item-inner .md-avatar-icon { margin-right: 20px; } [dir=rtl] md-list.md-dense md-list-item .md-avatar, [dir=rtl] md-list.md-dense md-list-item .md-avatar-icon, [dir=rtl] md-list.md-dense md-list-item .md-list-item-inner .md-avatar, [dir=rtl] md-list.md-dense md-list-item .md-list-item-inner .md-avatar-icon { margin-right: auto; margin-left: 20px; } md-list.md-dense md-list-item .md-avatar, md-list.md-dense md-list-item .md-list-item-inner .md-avatar { -webkit-box-flex: 0; -webkit-flex: none; flex: none; width: 36px; height: 36px; } md-list.md-dense md-list-item.md-2-line .md-list-item-text.md-offset, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, md-list.md-dense md-list-item.md-3-line .md-list-item-text.md-offset, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { margin-left: 56px; } [dir=rtl] md-list.md-dense md-list-item.md-2-line .md-list-item-text.md-offset, [dir=rtl] md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, [dir=rtl] md-list.md-dense md-list-item.md-3-line .md-list-item-text.md-offset, [dir=rtl] md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { margin-left: auto; margin-right: 56px; } md-list.md-dense md-list-item.md-2-line .md-list-item-text h3, md-list.md-dense md-list-item.md-2-line .md-list-item-text h4, md-list.md-dense md-list-item.md-2-line .md-list-item-text p, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text h3, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text h4, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text p, md-list.md-dense md-list-item.md-3-line .md-list-item-text h3, md-list.md-dense md-list-item.md-3-line .md-list-item-text h4, md-list.md-dense md-list-item.md-3-line .md-list-item-text p, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text h3, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text h4, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text p { line-height: 1.05; font-size: 12px; } md-list.md-dense md-list-item.md-2-line .md-list-item-text h3, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text h3, md-list.md-dense md-list-item.md-3-line .md-list-item-text h3, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text h3 { font-size: 13px; } md-list.md-dense md-list-item.md-2-line, md-list.md-dense md-list-item.md-2-line > .md-no-style { min-height: 60px; } md-list.md-dense md-list-item.md-2-line::before, md-list.md-dense md-list-item.md-2-line > .md-no-style::before { content: ''; min-height: 60px; visibility: hidden; display: inline-block; } md-list.md-dense md-list-item.md-2-line > .md-avatar, md-list.md-dense md-list-item.md-2-line .md-avatar-icon, md-list.md-dense md-list-item.md-2-line > .md-no-style > .md-avatar, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-avatar-icon { margin-top: 12px; } md-list.md-dense md-list-item.md-3-line, md-list.md-dense md-list-item.md-3-line > .md-no-style { min-height: 76px; } md-list.md-dense md-list-item.md-3-line::before, md-list.md-dense md-list-item.md-3-line > .md-no-style::before { content: ''; min-height: 76px; visibility: hidden; display: inline-block; } md-list.md-dense md-list-item.md-3-line > md-icon:first-child, md-list.md-dense md-list-item.md-3-line > .md-avatar, md-list.md-dense md-list-item.md-3-line > .md-no-style > md-icon:first-child, md-list.md-dense md-list-item.md-3-line > .md-no-style > .md-avatar { margin-top: 16px; } md-list-item { position: relative; } md-list-item.md-proxy-focus.md-focused .md-no-style { -webkit-transition: background-color 0.15s linear; transition: background-color 0.15s linear; } md-list-item._md-button-wrap { position: relative; } md-list-item._md-button-wrap > div.md-button:first-child { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; -webkit-box-pack: start; -webkit-justify-content: flex-start; justify-content: flex-start; padding: 0 16px; margin: 0; font-weight: 400; text-align: left; border: medium none; } [dir=rtl] md-list-item._md-button-wrap > div.md-button:first-child { text-align: right; } md-list-item._md-button-wrap > div.md-button:first-child > .md-button:first-child { position: absolute; top: 0; left: 0; height: 100%; margin: 0; padding: 0; } md-list-item._md-button-wrap > div.md-button:first-child .md-list-item-inner { width: 100%; min-height: inherit; } md-list-item.md-no-proxy, md-list-item .md-no-style { position: relative; padding: 0px 16px; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; } md-list-item.md-no-proxy.md-button, md-list-item .md-no-style.md-button { font-size: inherit; height: inherit; text-align: left; text-transform: none; width: 100%; white-space: normal; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: inherit; flex-direction: inherit; -webkit-box-align: inherit; -webkit-align-items: inherit; align-items: inherit; border-radius: 0; margin: 0; } [dir=rtl] md-list-item.md-no-proxy.md-button, [dir=rtl] md-list-item .md-no-style.md-button { text-align: right; } md-list-item.md-no-proxy.md-button > .md-ripple-container, md-list-item .md-no-style.md-button > .md-ripple-container { border-radius: 0; } md-list-item.md-no-proxy:focus, md-list-item .md-no-style:focus { outline: none; } md-list-item.md-clickable:hover { cursor: pointer; } md-list-item md-divider { position: absolute; bottom: 0; left: 0; width: 100%; } [dir=rtl] md-list-item md-divider { left: auto; right: 0; } md-list-item md-divider[md-inset] { left: 72px; width: calc(100% - 72px); margin: 0 !important; } [dir=rtl] md-list-item md-divider[md-inset] { left: auto; right: 72px; } md-list-item, md-list-item .md-list-item-inner { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: start; -webkit-justify-content: flex-start; justify-content: flex-start; -webkit-box-align: center; -webkit-align-items: center; align-items: center; min-height: 48px; height: auto; } md-list-item::before, md-list-item .md-list-item-inner::before { content: ''; min-height: 48px; visibility: hidden; display: inline-block; } md-list-item > div.md-primary > md-icon:not(.md-avatar-icon), md-list-item > div.md-secondary > md-icon:not(.md-avatar-icon), md-list-item > md-icon:first-child:not(.md-avatar-icon), md-list-item > md-icon.md-secondary:not(.md-avatar-icon), md-list-item .md-list-item-inner > div.md-primary > md-icon:not(.md-avatar-icon), md-list-item .md-list-item-inner > div.md-secondary > md-icon:not(.md-avatar-icon), md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon), md-list-item .md-list-item-inner > md-icon.md-secondary:not(.md-avatar-icon) { width: 24px; margin-top: 16px; margin-bottom: 12px; box-sizing: content-box; } md-list-item > div.md-primary > md-checkbox, md-list-item > div.md-secondary > md-checkbox, md-list-item > md-checkbox, md-list-item md-checkbox.md-secondary, md-list-item .md-list-item-inner > div.md-primary > md-checkbox, md-list-item .md-list-item-inner > div.md-secondary > md-checkbox, md-list-item .md-list-item-inner > md-checkbox, md-list-item .md-list-item-inner md-checkbox.md-secondary { -webkit-align-self: center; -ms-grid-row-align: center; align-self: center; } md-list-item > div.md-primary > md-checkbox .md-label, md-list-item > div.md-secondary > md-checkbox .md-label, md-list-item > md-checkbox .md-label, md-list-item md-checkbox.md-secondary .md-label, md-list-item .md-list-item-inner > div.md-primary > md-checkbox .md-label, md-list-item .md-list-item-inner > div.md-secondary > md-checkbox .md-label, md-list-item .md-list-item-inner > md-checkbox .md-label, md-list-item .md-list-item-inner md-checkbox.md-secondary .md-label { display: none; } md-list-item > md-icon:first-child:not(.md-avatar-icon), md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { margin-right: 32px; } [dir=rtl] md-list-item > md-icon:first-child:not(.md-avatar-icon), [dir=rtl] md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { margin-right: auto; margin-left: 32px; } md-list-item .md-avatar, md-list-item .md-avatar-icon, md-list-item .md-list-item-inner .md-avatar, md-list-item .md-list-item-inner .md-avatar-icon { margin-top: 8px; margin-bottom: 8px; margin-right: 16px; border-radius: 50%; box-sizing: content-box; } [dir=rtl] md-list-item .md-avatar, [dir=rtl] md-list-item .md-avatar-icon, [dir=rtl] md-list-item .md-list-item-inner .md-avatar, [dir=rtl] md-list-item .md-list-item-inner .md-avatar-icon { margin-right: auto; margin-left: 16px; } md-list-item .md-avatar, md-list-item .md-list-item-inner .md-avatar { -webkit-box-flex: 0; -webkit-flex: none; flex: none; width: 40px; height: 40px; } md-list-item .md-avatar-icon, md-list-item .md-list-item-inner .md-avatar-icon { padding: 8px; } md-list-item .md-avatar-icon svg, md-list-item .md-list-item-inner .md-avatar-icon svg { width: 24px; height: 24px; } md-list-item > md-checkbox, md-list-item .md-list-item-inner > md-checkbox { width: 24px; margin-left: 3px; margin-right: 29px; margin-top: 16px; } [dir=rtl] md-list-item > md-checkbox, [dir=rtl] md-list-item .md-list-item-inner > md-checkbox { margin-left: 29px; } [dir=rtl] md-list-item > md-checkbox, [dir=rtl] md-list-item .md-list-item-inner > md-checkbox { margin-right: 3px; } md-list-item .md-secondary-container, md-list-item .md-list-item-inner .md-secondary-container { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; -webkit-flex-shrink: 0; flex-shrink: 0; margin: auto; margin-right: 0; margin-left: auto; } [dir=rtl] md-list-item .md-secondary-container, [dir=rtl] md-list-item .md-list-item-inner .md-secondary-container { margin-right: auto; } [dir=rtl] md-list-item .md-secondary-container, [dir=rtl] md-list-item .md-list-item-inner .md-secondary-container { margin-left: 0; } md-list-item .md-secondary-container .md-button:last-of-type, md-list-item .md-secondary-container .md-icon-button:last-of-type, md-list-item .md-list-item-inner .md-secondary-container .md-button:last-of-type, md-list-item .md-list-item-inner .md-secondary-container .md-icon-button:last-of-type { margin-right: 0; } [dir=rtl] md-list-item .md-secondary-container .md-button:last-of-type, [dir=rtl] md-list-item .md-secondary-container .md-icon-button:last-of-type, [dir=rtl] md-list-item .md-list-item-inner .md-secondary-container .md-button:last-of-type, [dir=rtl] md-list-item .md-list-item-inner .md-secondary-container .md-icon-button:last-of-type { margin-right: auto; margin-left: 0; } md-list-item .md-secondary-container md-checkbox, md-list-item .md-list-item-inner .md-secondary-container md-checkbox { margin-top: 0; margin-bottom: 0; } md-list-item .md-secondary-container md-checkbox:last-child, md-list-item .md-list-item-inner .md-secondary-container md-checkbox:last-child { width: 24px; margin-right: 0; } [dir=rtl] md-list-item .md-secondary-container md-checkbox:last-child, [dir=rtl] md-list-item .md-list-item-inner .md-secondary-container md-checkbox:last-child { margin-right: auto; margin-left: 0; } md-list-item .md-secondary-container md-switch, md-list-item .md-list-item-inner .md-secondary-container md-switch { margin-top: 0; margin-bottom: 0; margin-right: -6px; } [dir=rtl] md-list-item .md-secondary-container md-switch, [dir=rtl] md-list-item .md-list-item-inner .md-secondary-container md-switch { margin-right: auto; margin-left: -6px; } md-list-item > p, md-list-item > .md-list-item-inner > p, md-list-item .md-list-item-inner > p, md-list-item .md-list-item-inner > .md-list-item-inner > p { -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; margin: 0; } md-list-item.md-2-line, md-list-item.md-2-line > .md-no-style, md-list-item.md-3-line, md-list-item.md-3-line > .md-no-style { -webkit-box-align: start; -webkit-align-items: flex-start; align-items: flex-start; -webkit-box-pack: center; -webkit-justify-content: center; justify-content: center; } md-list-item.md-2-line.md-long-text, md-list-item.md-2-line > .md-no-style.md-long-text, md-list-item.md-3-line.md-long-text, md-list-item.md-3-line > .md-no-style.md-long-text { margin-top: 8px; margin-bottom: 8px; } md-list-item.md-2-line .md-list-item-text, md-list-item.md-2-line > .md-no-style .md-list-item-text, md-list-item.md-3-line .md-list-item-text, md-list-item.md-3-line > .md-no-style .md-list-item-text { -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; margin: auto; text-overflow: ellipsis; overflow: hidden; } md-list-item.md-2-line .md-list-item-text.md-offset, md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, md-list-item.md-3-line .md-list-item-text.md-offset, md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { margin-left: 56px; } [dir=rtl] md-list-item.md-2-line .md-list-item-text.md-offset, [dir=rtl] md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, [dir=rtl] md-list-item.md-3-line .md-list-item-text.md-offset, [dir=rtl] md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { margin-left: auto; margin-right: 56px; } md-list-item.md-2-line .md-list-item-text h3, md-list-item.md-2-line > .md-no-style .md-list-item-text h3, md-list-item.md-3-line .md-list-item-text h3, md-list-item.md-3-line > .md-no-style .md-list-item-text h3 { font-size: 16px; font-weight: 400; letter-spacing: 0.010em; margin: 0 0 0px 0; line-height: 1.2em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } md-list-item.md-2-line .md-list-item-text h4, md-list-item.md-2-line > .md-no-style .md-list-item-text h4, md-list-item.md-3-line .md-list-item-text h4, md-list-item.md-3-line > .md-no-style .md-list-item-text h4 { font-size: 14px; letter-spacing: 0.010em; margin: 3px 0 1px 0; font-weight: 400; line-height: 1.2em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } md-list-item.md-2-line .md-list-item-text p, md-list-item.md-2-line > .md-no-style .md-list-item-text p, md-list-item.md-3-line .md-list-item-text p, md-list-item.md-3-line > .md-no-style .md-list-item-text p { font-size: 14px; font-weight: 500; letter-spacing: 0.010em; margin: 0 0 0 0; line-height: 1.6em; } md-list-item.md-2-line, md-list-item.md-2-line > .md-no-style { height: auto; min-height: 72px; } md-list-item.md-2-line::before, md-list-item.md-2-line > .md-no-style::before { content: ''; min-height: 72px; visibility: hidden; display: inline-block; } md-list-item.md-2-line > .md-avatar, md-list-item.md-2-line .md-avatar-icon, md-list-item.md-2-line > .md-no-style > .md-avatar, md-list-item.md-2-line > .md-no-style .md-avatar-icon { margin-top: 12px; } md-list-item.md-2-line > md-icon:first-child, md-list-item.md-2-line > .md-no-style > md-icon:first-child { -webkit-align-self: flex-start; align-self: flex-start; } md-list-item.md-2-line .md-list-item-text, md-list-item.md-2-line > .md-no-style .md-list-item-text { -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; } md-list-item.md-3-line, md-list-item.md-3-line > .md-no-style { height: auto; min-height: 88px; } md-list-item.md-3-line::before, md-list-item.md-3-line > .md-no-style::before { content: ''; min-height: 88px; visibility: hidden; display: inline-block; } md-list-item.md-3-line > md-icon:first-child, md-list-item.md-3-line > .md-avatar, md-list-item.md-3-line > .md-no-style > md-icon:first-child, md-list-item.md-3-line > .md-no-style > .md-avatar { margin-top: 16px; } .md-open-menu-container { position: fixed; left: 0; top: 0; z-index: 100; opacity: 0; border-radius: 2px; max-height: calc(100vh - 10px); overflow: auto; } .md-open-menu-container md-menu-divider { margin-top: 4px; margin-bottom: 4px; height: 1px; min-height: 1px; max-height: 1px; width: 100%; } .md-open-menu-container md-menu-content > * { opacity: 0; } .md-open-menu-container:not(.md-clickable) { pointer-events: none; } .md-open-menu-container.md-active { opacity: 1; -webkit-transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -webkit-transition-duration: 200ms; transition-duration: 200ms; } .md-open-menu-container.md-active > md-menu-content > * { opacity: 1; -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); -webkit-transition-duration: 200ms; transition-duration: 200ms; -webkit-transition-delay: 100ms; transition-delay: 100ms; } .md-open-menu-container.md-leave { opacity: 0; -webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); -webkit-transition-duration: 250ms; transition-duration: 250ms; } md-menu-content { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; padding: 8px 0; max-height: 304px; overflow-y: auto; } md-menu-content.md-dense { max-height: 208px; } md-menu-content.md-dense md-menu-item { height: 32px; min-height: 0px; } md-menu-item { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -webkit-flex-direction: row; flex-direction: row; min-height: 48px; height: 48px; -webkit-align-content: center; align-content: center; -webkit-box-pack: start; -webkit-justify-content: flex-start; justify-content: flex-start; /* * We cannot use flex on