├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── LICENSE.txt ├── OGL-LICENSE.txt ├── README.md ├── examples ├── 5e-srd │ ├── aboleth.json │ ├── acolyte.json │ ├── adult-black-dragon.json │ ├── adult-blue-dragon.json │ ├── adult-brass-dragon.json │ ├── adult-bronze-dragon.json │ ├── adult-copper-dragon.json │ ├── adult-gold-dragon.json │ ├── adult-green-dragon.json │ ├── adult-red-dragon.json │ ├── adult-silver-dragon.json │ ├── adult-white-dragon.json │ ├── air-elemental.json │ ├── ancient-black-dragon.json │ ├── ancient-blue-dragon.json │ ├── ancient-brass-dragon.json │ ├── ancient-bronze-dragon.json │ ├── ancient-copper-dragon.json │ ├── ancient-gold-dragon.json │ ├── ancient-green-dragon.json │ ├── ancient-red-dragon.json │ ├── ancient-silver-dragon.json │ ├── ancient-white-dragon.json │ ├── androsphinx.json │ ├── animated-armor.json │ ├── ankheg.json │ ├── ape.json │ ├── archmage.json │ ├── assassin.json │ ├── awakened-shrub.json │ ├── awakened-tree.json │ ├── axe-beak.json │ ├── azer.json │ ├── baboon.json │ ├── badger.json │ ├── balor.json │ ├── bandit-captain.json │ ├── bandit.json │ ├── barbed-devil.json │ ├── basilisk.json │ ├── bat.json │ ├── bearded-devil.json │ ├── behir.json │ ├── berserker.json │ ├── black-bear.json │ ├── black-dragon-wyrmling.json │ ├── black-pudding.json │ ├── blink-dog.json │ ├── blood-hawk.json │ ├── blue-dragon-wyrmling.json │ ├── boar.json │ ├── bone-devil.json │ ├── brass-dragon-wyrmling.json │ ├── bronze-dragon-wyrmling.json │ ├── brown-bear.json │ ├── bugbear.json │ ├── bulette.json │ ├── camel.json │ ├── cat.json │ ├── centaur.json │ ├── chain-devil.json │ ├── chimera.json │ ├── chuul.json │ ├── clay-golem.json │ ├── cloaker.json │ ├── cloud-giant.json │ ├── cockatrice.json │ ├── commoner.json │ ├── constrictor-snake.json │ ├── copper-dragon-wyrmling.json │ ├── couatl.json │ ├── crab.json │ ├── crocodile.json │ ├── cult-fanatic.json │ ├── cultist.json │ ├── darkmantle.json │ ├── death-dog.json │ ├── deep-gnome-svirfneblin.json │ ├── deer.json │ ├── deva.json │ ├── dire-wolf.json │ ├── djinni.json │ ├── doppelganger.json │ ├── draft-horse.json │ ├── dragon-turtle.json │ ├── dretch.json │ ├── drider.json │ ├── drow.json │ ├── druid.json │ ├── dryad.json │ ├── duergar.json │ ├── dust-mephit.json │ ├── eagle.json │ ├── earth-elemental.json │ ├── efreeti.json │ ├── elephant.json │ ├── elk.json │ ├── erinyes.json │ ├── ettercap.json │ ├── ettin.json │ ├── fire-elemental.json │ ├── fire-giant.json │ ├── flesh-golem.json │ ├── flying-snake.json │ ├── flying-sword.json │ ├── frog.json │ ├── frost-giant.json │ ├── gargoyle.json │ ├── gelatinous-cube.json │ ├── ghast.json │ ├── ghost.json │ ├── ghoul.json │ ├── giant-ape.json │ ├── giant-badger.json │ ├── giant-bat.json │ ├── giant-boar.json │ ├── giant-centipede.json │ ├── giant-constrictor-snake.json │ ├── giant-crab.json │ ├── giant-crocodile.json │ ├── giant-eagle.json │ ├── giant-elk.json │ ├── giant-fire-beetle.json │ ├── giant-frog.json │ ├── giant-goat.json │ ├── giant-hyena.json │ ├── giant-lizard.json │ ├── giant-octopus.json │ ├── giant-owl.json │ ├── giant-poisonous-snake.json │ ├── giant-rat-diseased.json │ ├── giant-rat.json │ ├── giant-scorpion.json │ ├── giant-sea-horse.json │ ├── giant-shark.json │ ├── giant-spider.json │ ├── giant-toad.json │ ├── giant-vulture.json │ ├── giant-wasp.json │ ├── giant-weasel.json │ ├── giant-wolf-spider.json │ ├── gibbering-mouther.json │ ├── glabrezu.json │ ├── gladiator.json │ ├── gnoll.json │ ├── goat.json │ ├── goblin.json │ ├── gold-dragon-wyrmling.json │ ├── gorgon.json │ ├── gray-ooze.json │ ├── green-dragon-wyrmling.json │ ├── green-hag.json │ ├── grick.json │ ├── griffon.json │ ├── grimlock.json │ ├── guard.json │ ├── guardian-naga.json │ ├── gynosphinx.json │ ├── half-red-dragon-veteran.json │ ├── harpy.json │ ├── hawk.json │ ├── hell-hound.json │ ├── hezrou.json │ ├── hill-giant.json │ ├── hippogriff.json │ ├── hobgoblin.json │ ├── homunculus.json │ ├── horned-devil.json │ ├── hunter-shark.json │ ├── hydra.json │ ├── hyena.json │ ├── ice-devil.json │ ├── ice-mephit.json │ ├── imp.json │ ├── invisible-stalker.json │ ├── iron-golem.json │ ├── jackal.json │ ├── killer-whale.json │ ├── knight.json │ ├── kobold.json │ ├── kraken.json │ ├── lamia.json │ ├── lemure.json │ ├── lich.json │ ├── lion.json │ ├── lizard.json │ ├── lizardfolk.json │ ├── mage.json │ ├── magma-mephit.json │ ├── magmin.json │ ├── mammoth.json │ ├── manticore.json │ ├── marilith.json │ ├── mastiff.json │ ├── medusa.json │ ├── merfolk.json │ ├── merrow.json │ ├── mimic.json │ ├── minotaur-skeleton.json │ ├── minotaur.json │ ├── mule.json │ ├── mummy-lord.json │ ├── mummy.json │ ├── nalfeshnee.json │ ├── night-hag.json │ ├── nightmare.json │ ├── noble.json │ ├── ochre-jelly.json │ ├── octopus.json │ ├── ogre-zombie.json │ ├── ogre.json │ ├── oni.json │ ├── orc.json │ ├── otyugh.json │ ├── owl.json │ ├── owlbear.json │ ├── panther.json │ ├── pegasus.json │ ├── phase-spider.json │ ├── pit-fiend.json │ ├── planetar.json │ ├── plesiosaurus.json │ ├── poisonous-snake.json │ ├── polar-bear.json │ ├── pony.json │ ├── priest.json │ ├── pseudodragon.json │ ├── purple-worm.json │ ├── quasit.json │ ├── quipper.json │ ├── rakshasa.json │ ├── rat.json │ ├── raven.json │ ├── red-dragon-wyrmling.json │ ├── reef-shark.json │ ├── remorhaz.json │ ├── rhinoceros.json │ ├── riding-horse.json │ ├── roc.json │ ├── roper.json │ ├── rug-of-smothering.json │ ├── rust-monster.json │ ├── saber-toothed-tiger.json │ ├── sahuagin.json │ ├── salamander.json │ ├── satyr.json │ ├── scorpion.json │ ├── scout.json │ ├── sea-hag.json │ ├── sea-horse.json │ ├── shadow.json │ ├── shambling-mound.json │ ├── shield-guardian.json │ ├── shrieker.json │ ├── silver-dragon-wyrmling.json │ ├── skeleton.json │ ├── solar.json │ ├── specter.json │ ├── spider.json │ ├── spirit-naga.json │ ├── sprite.json │ ├── spy.json │ ├── steam-mephit.json │ ├── stirge.json │ ├── stone-giant.json │ ├── stone-golem.json │ ├── storm-giant.json │ ├── succubus-incubus.json │ ├── swarm-of-bats.json │ ├── swarm-of-beetles.json │ ├── swarm-of-centipedes.json │ ├── swarm-of-insects.json │ ├── swarm-of-poisonous-snakes.json │ ├── swarm-of-quippers.json │ ├── swarm-of-rats.json │ ├── swarm-of-ravens.json │ ├── swarm-of-spiders.json │ ├── swarm-of-wasps.json │ ├── tarrasque.json │ ├── thug.json │ ├── tiger.json │ ├── treant.json │ ├── tribal-warrior.json │ ├── triceratops.json │ ├── troll.json │ ├── tyrannosaurus-rex.json │ ├── unicorn.json │ ├── vampire-spawn.json │ ├── vampire.json │ ├── veteran.json │ ├── violet-fungus.json │ ├── vrock.json │ ├── vulture.json │ ├── warhorse-skeleton.json │ ├── warhorse.json │ ├── water-elemental.json │ ├── weasel.json │ ├── werebear.json │ ├── wereboar.json │ ├── wererat.json │ ├── weretiger.json │ ├── werewolf.json │ ├── white-dragon-wyrmling.json │ ├── wight.json │ ├── will-o-wisp.json │ ├── winter-wolf.json │ ├── wolf.json │ ├── worg.json │ ├── wraith.json │ ├── wyvern.json │ ├── xorn.json │ ├── young-black-dragon.json │ ├── young-blue-dragon.json │ ├── young-brass-dragon.json │ ├── young-bronze-dragon.json │ ├── young-copper-dragon.json │ ├── young-gold-dragon.json │ ├── young-green-dragon.json │ ├── young-red-dragon.json │ ├── young-silver-dragon.json │ ├── young-white-dragon.json │ └── zombie.json └── homebrew │ ├── dragonborn-slaver.json │ ├── order-of-the-falcon-knight.json │ ├── vishan-delaar.json │ └── wilder-scout.json ├── images ├── markdown-buttons.png ├── math-expression-menus.png ├── name-expression-menus.png ├── statblock.png └── title-section.png ├── index.html ├── package-lock.json ├── package.json ├── src ├── css │ ├── elements │ │ ├── autonomous │ │ │ ├── containers │ │ │ │ ├── advanced-stats.css │ │ │ │ ├── basic-stats.css │ │ │ │ ├── bottom-stats.css │ │ │ │ ├── heading-stats.css │ │ │ │ ├── stat-block-editor.css │ │ │ │ ├── stat-block-menu.css │ │ │ │ ├── stat-block-sidebar.css │ │ │ │ ├── stat-block.css │ │ │ │ └── top-stats.css │ │ │ ├── dialogs │ │ │ │ ├── custom-dialog.css │ │ │ │ ├── export-dialog.css │ │ │ │ ├── generate-attack-dialog.css │ │ │ │ ├── generate-spellcasting-dialog.css │ │ │ │ ├── import-dialog.css │ │ │ │ ├── import-json-dialog.css │ │ │ │ ├── import-open5e-dialog.css │ │ │ │ ├── import-srd-dialog.css │ │ │ │ ├── option-dialog.css │ │ │ │ └── reset-dialog.css │ │ │ ├── error-messages.css │ │ │ ├── getting-started-help-box.css │ │ │ ├── lists │ │ │ │ ├── display-block-list.css │ │ │ │ ├── display-block.css │ │ │ │ ├── drag-and-drop-list-item.css │ │ │ │ ├── editable-block-list.css │ │ │ │ ├── editable-block.css │ │ │ │ ├── legendary-action-display-block-list.css │ │ │ │ ├── legendary-action-display-block.css │ │ │ │ ├── legendary-action-editable-block-list.css │ │ │ │ ├── legendary-action-editable-block.css │ │ │ │ ├── property-list-item.css │ │ │ │ └── property-list.css │ │ │ ├── loading-screen.css │ │ │ ├── menus │ │ │ │ ├── drop-down-menu.css │ │ │ │ └── expression-menu.css │ │ │ ├── property-block.css │ │ │ ├── property-line.css │ │ │ ├── section-divider.css │ │ │ ├── sections │ │ │ │ ├── ability-scores-section.css │ │ │ │ ├── actions-section.css │ │ │ │ ├── armor-class-section.css │ │ │ │ ├── block-list-section.css │ │ │ │ ├── challenge-rating-section.css │ │ │ │ ├── condition-immunities-section.css │ │ │ │ ├── damage-immunities-section.css │ │ │ │ ├── damage-resistances-section.css │ │ │ │ ├── damage-vulnerabilities-section.css │ │ │ │ ├── hit-points-section.css │ │ │ │ ├── languages-section.css │ │ │ │ ├── legendary-actions-section.css │ │ │ │ ├── property-list-section.css │ │ │ │ ├── reactions-section.css │ │ │ │ ├── saving-throws-section.css │ │ │ │ ├── section.css │ │ │ │ ├── senses-section.css │ │ │ │ ├── skills-section.css │ │ │ │ ├── special-traits-section.css │ │ │ │ ├── speed-section.css │ │ │ │ ├── subtitle-section.css │ │ │ │ └── title-section.css │ │ │ ├── slide-toggle.css │ │ │ ├── spell-category-box.css │ │ │ ├── tapered-rule.css │ │ │ └── tooltips │ │ │ │ ├── custom-text-help-tooltip.css │ │ │ │ └── help-tooltip.css │ │ └── ui-controls.css │ ├── index.css │ ├── section-animations.css │ └── vendor │ │ └── material-icons.css ├── html │ ├── elements │ │ └── autonomous │ │ │ ├── containers │ │ │ ├── advanced-stats.html │ │ │ ├── basic-stats.html │ │ │ ├── bottom-stats.html │ │ │ ├── heading-stats.html │ │ │ ├── stat-block-editor.html │ │ │ ├── stat-block-menu.html │ │ │ ├── stat-block-sidebar.html │ │ │ ├── stat-block.html │ │ │ └── top-stats.html │ │ │ ├── dialogs │ │ │ ├── custom-dialog.html │ │ │ ├── export-dialog.html │ │ │ ├── generate-attack-dialog.html │ │ │ ├── generate-spellcasting-dialog.html │ │ │ ├── import-dialog.html │ │ │ ├── import-json-dialog.html │ │ │ ├── import-open5e-dialog.html │ │ │ ├── import-srd-dialog.html │ │ │ ├── option-dialog.html │ │ │ └── reset-dialog.html │ │ │ ├── error-messages.html │ │ │ ├── getting-started-help-box.html │ │ │ ├── lists │ │ │ ├── display-block-list.html │ │ │ ├── display-block.html │ │ │ ├── drag-and-drop-list-item.html │ │ │ ├── editable-block-list.html │ │ │ ├── editable-block.html │ │ │ ├── legendary-action-display-block-list.html │ │ │ ├── legendary-action-display-block.html │ │ │ ├── legendary-action-editable-block-list.html │ │ │ ├── legendary-action-editable-block.html │ │ │ ├── property-list-item.html │ │ │ └── property-list.html │ │ │ ├── loading-screen.html │ │ │ ├── menus │ │ │ ├── drop-down-menu.html │ │ │ └── expression-menu.html │ │ │ ├── property-block.html │ │ │ ├── property-line.html │ │ │ ├── section-divider.html │ │ │ ├── sections │ │ │ ├── ability-scores-section.html │ │ │ ├── actions-section.html │ │ │ ├── armor-class-section.html │ │ │ ├── block-list-section.html │ │ │ ├── challenge-rating-section.html │ │ │ ├── condition-immunities-section.html │ │ │ ├── damage-immunities-section.html │ │ │ ├── damage-resistances-section.html │ │ │ ├── damage-vulnerabilities-section.html │ │ │ ├── hit-points-section.html │ │ │ ├── languages-section.html │ │ │ ├── legendary-actions-section.html │ │ │ ├── property-list-section.html │ │ │ ├── reactions-section.html │ │ │ ├── saving-throws-section.html │ │ │ ├── section.html │ │ │ ├── senses-section.html │ │ │ ├── skills-section.html │ │ │ ├── special-traits-section.html │ │ │ ├── speed-section.html │ │ │ ├── subtitle-section.html │ │ │ └── title-section.html │ │ │ ├── slide-toggle.html │ │ │ ├── spell-category-box.html │ │ │ ├── tapered-rule.html │ │ │ └── tooltips │ │ │ ├── custom-text-help-tooltip.html │ │ │ └── help-tooltip.html │ └── export-inlined.html └── js │ ├── api │ ├── __mocks__ │ │ └── open5e-client.js │ └── open5e-client.js │ ├── data │ ├── challenge-rating-to-experience-points.js │ ├── challenge-rating-to-proficiency-bonus.js │ ├── conditions.js │ ├── creature-alignments.js │ ├── creature-sizes-to-hit-die-sizes.js │ ├── creature-tags.js │ ├── creature-types.js │ ├── damage-types-for-property-lists.js │ ├── damage-types.js │ ├── languages.js │ ├── predefined-weapons.js │ ├── spellcaster-types.js │ ├── spells.js │ └── srd-creature-list.js │ ├── elements │ ├── autonomous │ │ ├── containers │ │ │ ├── advanced-stats.js │ │ │ ├── basic-stats.js │ │ │ ├── bottom-stats.js │ │ │ ├── divisible-container.js │ │ │ ├── heading-stats.js │ │ │ ├── stat-block-editor.js │ │ │ ├── stat-block-menu.js │ │ │ ├── stat-block-menu.test.js │ │ │ ├── stat-block-sidebar.js │ │ │ ├── stat-block-sidebar.test.js │ │ │ ├── stat-block.js │ │ │ ├── stats-container.js │ │ │ └── top-stats.js │ │ ├── custom-autonomous-element.js │ │ ├── dialogs │ │ │ ├── custom-dialog.js │ │ │ ├── export-dialog.js │ │ │ ├── export-dialog.test.js │ │ │ ├── generate-attack-dialog.js │ │ │ ├── generate-attack-dialog.test.js │ │ │ ├── generate-spellcasting-dialog.js │ │ │ ├── generate-spellcasting-dialog.test.js │ │ │ ├── import-dialog.js │ │ │ ├── import-json-dialog.js │ │ │ ├── import-json-dialog.test.js │ │ │ ├── import-open5e-dialog.js │ │ │ ├── import-open5e-dialog.test.js │ │ │ ├── import-srd-dialog.js │ │ │ ├── import-srd-dialog.test.js │ │ │ ├── option-dialog.js │ │ │ ├── reset-dialog.js │ │ │ └── reset-dialog.test.js │ │ ├── error-messages.js │ │ ├── error-messages.test.js │ │ ├── getting-started-help-box.js │ │ ├── lists │ │ │ ├── display-block-list.js │ │ │ ├── display-block.js │ │ │ ├── drag-and-drop-list-item.js │ │ │ ├── drag-and-drop-list.js │ │ │ ├── editable-block-list.js │ │ │ ├── editable-block.js │ │ │ ├── editable-block.test.js │ │ │ ├── legendary-action-display-block-list.js │ │ │ ├── legendary-action-display-block.js │ │ │ ├── legendary-action-editable-block-list.js │ │ │ ├── legendary-action-editable-block.js │ │ │ ├── property-list-item.js │ │ │ └── property-list.js │ │ ├── loading-screen.js │ │ ├── menus │ │ │ ├── drop-down-menu.js │ │ │ └── expression-menu.js │ │ ├── property-block.js │ │ ├── property-line.js │ │ ├── section-divider.js │ │ ├── sections │ │ │ ├── ability-scores-section.js │ │ │ ├── ability-scores-section.test.js │ │ │ ├── actions-section.js │ │ │ ├── actions-section.test.js │ │ │ ├── armor-class-section.js │ │ │ ├── armor-class-section.test.js │ │ │ ├── block-list-section.js │ │ │ ├── block-list-section.specs.js │ │ │ ├── challenge-rating-section.js │ │ │ ├── challenge-rating-section.test.js │ │ │ ├── condition-immunities-section.js │ │ │ ├── condition-immunities-section.test.js │ │ │ ├── damage-immunities-section.js │ │ │ ├── damage-immunities-section.test.js │ │ │ ├── damage-resistances-section.js │ │ │ ├── damage-resistances-section.test.js │ │ │ ├── damage-vulnerabilities-section.js │ │ │ ├── damage-vulnerabilities-section.test.js │ │ │ ├── hit-points-section.js │ │ │ ├── hit-points-section.test.js │ │ │ ├── languages-section.js │ │ │ ├── languages-section.test.js │ │ │ ├── legendary-actions-section.js │ │ │ ├── legendary-actions-section.test.js │ │ │ ├── property-line-section.js │ │ │ ├── property-list-section.js │ │ │ ├── property-list-section.specs.js │ │ │ ├── reactions-section.js │ │ │ ├── reactions-section.test.js │ │ │ ├── saving-throws-section.js │ │ │ ├── saving-throws-section.test.js │ │ │ ├── section.js │ │ │ ├── senses-section.js │ │ │ ├── senses-section.test.js │ │ │ ├── skills-section.js │ │ │ ├── skills-section.test.js │ │ │ ├── special-traits-section.js │ │ │ ├── special-traits-section.test.js │ │ │ ├── speed-section.js │ │ │ ├── speed-section.test.js │ │ │ ├── subtitle-section.js │ │ │ ├── subtitle-section.test.js │ │ │ ├── title-section.js │ │ │ └── title-section.test.js │ │ ├── slide-toggle.js │ │ ├── spell-category-box.js │ │ ├── tapered-rule.js │ │ └── tooltips │ │ │ ├── custom-text-help-tooltip.js │ │ │ └── help-tooltip.js │ └── builtin │ │ ├── block-textarea.js │ │ ├── dynamic-select.js │ │ ├── enable-disable-elements-checkbox.js │ │ ├── number-input.js │ │ ├── number-select.js │ │ ├── property-datalist.js │ │ ├── sanitized-paragraph.js │ │ └── text-input.js │ ├── helpers │ ├── array-helpers.js │ ├── element-helpers.js │ ├── export-helpers.js │ ├── file-helpers.js │ ├── html-export-document-factory.js │ ├── html-templates.js │ ├── is-running-in-jsdom.js │ ├── local-storage-proxy.js │ ├── markdown-helpers.js │ ├── number-helpers.js │ ├── object-helpers.js │ ├── print-helpers.js │ ├── sanitize-html.js │ ├── spell-helpers.js │ ├── string-formatter.js │ └── test │ │ ├── event-interceptor.js │ │ ├── expect-matchers.js │ │ ├── matchers │ │ ├── property-line-matchers.js │ │ ├── to-be-in-mode.js │ │ ├── to-be-selected.js │ │ ├── to-have-elements-enabled-or-disabled-based-on-checkbox-state.js │ │ └── to-have-error.js │ │ ├── test-custom-elements.js │ │ └── test-globals.js │ ├── init.js │ ├── models │ ├── abilities.js │ ├── armor-class.js │ ├── attack.js │ ├── challenge-rating.js │ ├── creature.js │ ├── current-context.js │ ├── hit-points.js │ ├── lists │ │ ├── block │ │ │ ├── actions.js │ │ │ ├── block-list-model.js │ │ │ ├── block-model.js │ │ │ ├── legendary-actions.js │ │ │ ├── legendary-block-model.js │ │ │ ├── reactions.js │ │ │ └── special-traits.js │ │ └── property │ │ │ ├── condition-immunities.js │ │ │ ├── damage-immunities.js │ │ │ ├── damage-resistances.js │ │ │ ├── damage-vulnerabilities.js │ │ │ ├── languages.js │ │ │ └── property-list-model.js │ ├── model.js │ ├── property-line-model.js │ ├── saving-throws.js │ ├── senses.js │ ├── settings │ │ ├── layout-settings.js │ │ └── local-settings.js │ ├── skills.js │ ├── speed.js │ ├── spellcasting.js │ ├── subtitle.js │ └── title.js │ └── parsers │ ├── grammars │ ├── markdown-grammar.pegjs │ ├── math-grammar.pegjs │ └── name-grammar.pegjs │ ├── markdown-parser.js │ ├── markdown-parser.test.js │ ├── math-parser.js │ ├── math-parser.test.js │ ├── name-parser.js │ ├── name-parser.test.js │ ├── parser.js │ └── scripts │ └── generate-parser-script.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "transform-es2015-modules-commonjs" 6 | ] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/js/parsers/markdown-parser.js 2 | src/js/parsers/name-parser.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "amd": true, 6 | "jest" : true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "global": "readable", 11 | "process": "readable", 12 | "module": "readable" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 2, { "CallExpression": {"arguments": "first"} } 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "windows" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ] 35 | } 36 | }; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | 17 | steps: 18 | - name: Checkout files 19 | uses: actions/checkout@v2 20 | - name: Setup Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: npm install 26 | - name: Run tests 27 | run: npm test 28 | - name: Create production bundle 29 | run: npm run build -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [16.x] 15 | 16 | steps: 17 | - name: Checkout files 18 | uses: actions/checkout@v2 19 | - name: Setup Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install dependencies 24 | run: npm install 25 | - name: Run tests 26 | run: npm test 27 | - name: Create production bundle 28 | run: npm run build 29 | - name: Deploy to GitHub Pages 30 | uses: JamesIves/github-pages-deploy-action@4.1.3 31 | with: 32 | ssh-key: ${{ secrets.DEPLOY_KEY }} 33 | branch: gh-pages 34 | folder: . 35 | clean: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .vscode/ 4 | *.code-workspace -------------------------------------------------------------------------------- /images/markdown-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/images/markdown-buttons.png -------------------------------------------------------------------------------- /images/math-expression-menus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/images/math-expression-menus.png -------------------------------------------------------------------------------- /images/name-expression-menus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/images/name-expression-menus.png -------------------------------------------------------------------------------- /images/statblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/images/statblock.png -------------------------------------------------------------------------------- /images/title-section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/images/title-section.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Statblock5e Creator 6 | 7 | 9 | 11 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/advanced-stats.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/containers/advanced-stats.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/basic-stats.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/containers/basic-stats.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/bottom-stats.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/containers/bottom-stats.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/heading-stats.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/containers/heading-stats.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/stat-block-editor.css: -------------------------------------------------------------------------------- 1 | :host { 2 | font-family: inherit; 3 | } 4 | 5 | .stat-block-editor__components { 6 | display: grid; 7 | grid-template-columns: auto 1fr; 8 | grid-template-rows: auto 1fr; 9 | } 10 | 11 | 12 | .stat-block-editor__stat-block-menu { 13 | grid-area: 1 / 1 / 2 / 3; 14 | } 15 | 16 | .stat-block-editor__stat-block-sidebar { 17 | grid-area: 2 / 1; 18 | } 19 | 20 | .stat-block-editor__stat-block { 21 | grid-area: 2 / 2; 22 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/stat-block-menu.css: -------------------------------------------------------------------------------- 1 | .stat-block-menu { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | position: fixed; 6 | top: 0; 7 | background: #606060; 8 | width: 100%; 9 | height: 48px; 10 | z-index: 2; 11 | } 12 | 13 | .stat-block-menu__controls { 14 | display: flex; 15 | width: 420px; 16 | margin-left: 95px; 17 | } 18 | 19 | .stat-block-menu__control-container { 20 | flex: 1 1 0; 21 | display: flex; 22 | flex-direction: column; 23 | text-align: center; 24 | margin-left: 2px; 25 | margin-right: 2px; 26 | } 27 | 28 | .stat-block-menu__control-subcontainer { 29 | flex: 1 1 auto; 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: center; 33 | align-items: stretch; 34 | height: 24px; 35 | } 36 | 37 | .stat-block-menu__dropdowns { 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: space-between; 41 | margin-left: 30px; 42 | width: 400px; 43 | } 44 | 45 | .stat-block-menu__menu-items { 46 | display: flex; 47 | flex-direction: column; 48 | } 49 | 50 | .stat-block-menu__label { 51 | color: white; 52 | font-weight: bold; 53 | } 54 | 55 | .stat-block-menu__all-sections-button { 56 | flex: 1 1 auto; 57 | width: 100%; 58 | display: inline-flex; 59 | justify-content: space-evenly; 60 | } 61 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/stat-block-sidebar.css: -------------------------------------------------------------------------------- 1 | .stat-block-sidebar { 2 | display: flex; 3 | flex-direction: column; 4 | background: #606060; 5 | width: 60px; 6 | min-height: 100%; 7 | margin: 0; 8 | padding-top: 0; 9 | padding-bottom: 0; 10 | padding-left: 5px; 11 | padding-right: 5px; 12 | opacity: 1; 13 | visibility: visible; 14 | transition: opacity 250ms ease-out, 15 | visibility 250ms linear 16 | } 17 | 18 | .stat-block-sidebar_hidden { 19 | opacity: 0; 20 | visibility: hidden; 21 | transition: opacity 250ms ease-in, 22 | visibility 250ms linear 23 | } 24 | 25 | .stat-block-sidebar__top-container { 26 | display: flex; 27 | flex: 0 1 auto; 28 | flex-direction: column; 29 | justify-content: flex-end; 30 | height: 210px; 31 | } 32 | 33 | .stat-block-sidebar__height-mode-toggle { 34 | height: 42px; 35 | } 36 | 37 | .stat-block-sidebar__label { 38 | color: white; 39 | font-weight: bold; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .stat-block-sidebar__slider-container { 44 | width: 60px; 45 | height: 860px; 46 | margin-top: 20px; 47 | margin-bottom: 20px; 48 | } 49 | 50 | .stat-block-sidebar__slider-container_hidden { 51 | display: none; 52 | } 53 | 54 | .stat-block-sidebar__slider { 55 | width: 860px; 56 | height: 60px; 57 | margin: 0; 58 | transform-origin: top left; 59 | transform: rotate(90deg) translatey(-60px); 60 | } 61 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/stat-block.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | justify-content: flex-start; 4 | position: relative; 5 | } 6 | 7 | .container { 8 | display: inline-block; 9 | margin-top: 60px; 10 | margin-left: 25px; 11 | margin-right: 25px; 12 | } 13 | 14 | .bar { 15 | height: 5px; 16 | background: #E69A28; 17 | border: 1px solid #000; 18 | } 19 | 20 | .content-wrap { 21 | background: #FDF1DC; 22 | padding: 0.6em; 23 | padding-bottom: 0.5em; 24 | border: 1px #DDD solid; 25 | box-shadow: 0 0 1.5em #867453; 26 | 27 | /* We don't want the box-shadow in front of the bar divs. */ 28 | z-index: 0; 29 | 30 | /* Leaving room for the two bars to protrude outwards */ 31 | margin-left: 2px; 32 | margin-right: 2px; 33 | 34 | /* This is possibly overriden by next CSS rule. */ 35 | width: 400px; 36 | 37 | column-width: 400px; 38 | column-gap: 40px; 39 | column-count: 1; 40 | 41 | /* We can't use CSS3 attr() here because no browser currently supports it, 42 | but we can use a CSS custom property instead. */ 43 | height: var(--statblock-content-height); 44 | 45 | /* By default, balance the content between the columns */ 46 | column-fill: balance-all; 47 | } 48 | 49 | :host([data-two-column]) .content-wrap { 50 | column-count: 2; 51 | 52 | /* One column is 400px and the gap between them is 40px. */ 53 | width: 840px; 54 | } 55 | 56 | :host([style^="--statblock-content-height"]) .content-wrap { 57 | /* When the height is manually set, fill columns sequentially. */ 58 | column-fill: auto; 59 | } 60 | 61 | .overflow-hide { 62 | flex: 1 1 auto; 63 | background: white; 64 | z-index: 1; 65 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/containers/top-stats.css: -------------------------------------------------------------------------------- 1 | * { 2 | color: #7A200D; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/custom-dialog.css: -------------------------------------------------------------------------------- 1 | .custom-dialog { 2 | max-width: 450px; 3 | border: none; 4 | box-shadow: 0 0 1.5em #867453; 5 | padding: 0; 6 | } 7 | 8 | .custom-dialog__title-bar { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: space-between; 13 | background: #606060; 14 | color: white; 15 | padding-left: 1em; 16 | } 17 | 18 | .custom-dialog__title { 19 | margin: 0; 20 | } 21 | 22 | .custom-dialog__close-button { 23 | display: inline-flex; 24 | align-items: center; 25 | justify-content: center; 26 | background: #606060; 27 | color: white; 28 | border: none; 29 | padding: 1em; 30 | cursor: pointer; 31 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/export-dialog.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/dialogs/export-dialog.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/import-dialog.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/dialogs/import-dialog.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/import-json-dialog.css: -------------------------------------------------------------------------------- 1 | .import-json-dialog__file-input { 2 | display: none; 3 | } 4 | 5 | .import-json-dialog__file-upload-drop-zone { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | border: medium dashed #aaaaaa; 11 | height: 417px; 12 | margin: 0 1em 1em 1em; 13 | } 14 | 15 | .import-json-dialog__file-upload-drop-zone_drag-enter { 16 | background: #90FF70; 17 | } 18 | 19 | .import-json-dialog__file-upload-drop-zone-label { 20 | font-weight: bold; 21 | } 22 | 23 | .import-json-dialog__choose-file-button { 24 | padding: 5px; 25 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/import-open5e-dialog.css: -------------------------------------------------------------------------------- 1 | .import-open5e-dialog__creature-select { 2 | margin: 1em; 3 | } 4 | 5 | .import-open5e-dialog__source-container { 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: center; 9 | padding-top: 0.5em; 10 | } 11 | 12 | .import-open5e-dialog__source-label { 13 | padding: 5px; 14 | margin: 5px; 15 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/import-srd-dialog.css: -------------------------------------------------------------------------------- 1 | .import-srd-dialog__creature-select { 2 | margin: 1em; 3 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/option-dialog.css: -------------------------------------------------------------------------------- 1 | .option-dialog { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .option-dialog__message { 7 | padding: 1em; 8 | margin: 0; 9 | } 10 | 11 | .option-dialog__status-container { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | 18 | .option-dialog__status-label { 19 | font-weight: bold; 20 | white-space: pre-line; 21 | padding: 0 1em; 22 | margin: 0; 23 | } 24 | 25 | .option-dialog__status-label_success { 26 | color: green; 27 | } 28 | 29 | .option-dialog__status-label_error { 30 | color: red; 31 | } 32 | 33 | .option-dialog__actions { 34 | display: flex; 35 | flex-direction: row; 36 | justify-content: center; 37 | padding: 1em; 38 | } 39 | 40 | .option-dialog__action-button { 41 | padding: 5px; 42 | margin-left: 5px; 43 | margin-right: 5px; 44 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/dialogs/reset-dialog.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/dialogs/reset-dialog.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/error-messages.css: -------------------------------------------------------------------------------- 1 | .error-messages { 2 | background: #EEEEEE; 3 | color: red; 4 | border: 1px solid black; 5 | font-weight: bold; 6 | margin-bottom: 10px; 7 | } 8 | 9 | .error-messages_hidden { 10 | display: none; 11 | } 12 | 13 | .error-messages__list { 14 | list-style-type: none; 15 | white-space: pre-wrap; 16 | padding: 5px; 17 | margin: 0; 18 | } 19 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/getting-started-help-box.css: -------------------------------------------------------------------------------- 1 | .getting-started-help-box { 2 | position: relative; 3 | background: #eeeeee; 4 | color: black; 5 | border: 1px solid black; 6 | white-space: normal; 7 | width: 500px; 8 | margin-top: 60px; 9 | margin-left: 5px; 10 | padding: 5px; 11 | } 12 | 13 | .getting-started-help-box_hidden { 14 | display: none; 15 | } 16 | 17 | .getting-started-help-box__close-button { 18 | position: absolute; 19 | top: 0px; 20 | right: 0px; 21 | display: inline-flex; 22 | align-items: center; 23 | justify-content: center; 24 | background: transparent; 25 | color: black; 26 | border: none; 27 | padding: 0.5em; 28 | cursor: pointer; 29 | } 30 | 31 | .getting-started-help-box h3:first-child { 32 | margin-top: 0; 33 | } 34 | 35 | .getting-started-help-box h4 { 36 | margin-bottom: 0; 37 | } 38 | 39 | .getting-started-help-box ul { 40 | margin: 0; 41 | } 42 | 43 | .getting-started-help-box table { 44 | margin-left: 2em; 45 | } 46 | 47 | .getting-started-help-box code { 48 | background: white; 49 | border-radius: 3px; 50 | font-family: monospace; 51 | padding: 0 3px; 52 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/display-block-list.css: -------------------------------------------------------------------------------- 1 | /* Last item should have no bottom margin as it takes too much whitespace. */ 2 | ::slotted(*:last-child) { 3 | margin-bottom: 0; 4 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/display-block.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin-top: 0.3em; 3 | margin-bottom: 0.9em; 4 | } 5 | 6 | .display-block { 7 | margin-top: inherit; 8 | margin-bottom: inherit; 9 | line-height: 1.5; 10 | display: block; 11 | } 12 | 13 | .display-block__name { 14 | margin: 0; 15 | display: inline; 16 | font-weight: bold; 17 | font-style: italic; 18 | } 19 | 20 | .display-block__text { 21 | display: inline; 22 | text-indent: 0; 23 | margin: 0; 24 | white-space: pre-wrap; 25 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/drag-and-drop-list-item.css: -------------------------------------------------------------------------------- 1 | .drag-and-drop-list-item__container { 2 | border: 1px solid Gray; 3 | } 4 | 5 | .drag-and-drop-list-item__container_dragover-top { 6 | border-top: solid 10px DodgerBlue; 7 | } 8 | 9 | .drag-and-drop-list-item__container_dragover-bottom { 10 | border-bottom: solid 10px DodgerBlue; 11 | } 12 | 13 | .drag-and-drop-list-item__drag-handle:hover { 14 | background: #90FFFF; 15 | cursor: move; 16 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/editable-block-list.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/editable-block.css: -------------------------------------------------------------------------------- 1 | .editable-block__container { 2 | display: grid; 3 | grid-template-columns: 34px auto; 4 | grid-template-rows: auto auto auto; 5 | grid-column-gap: 5px; 6 | grid-row-gap: 5px; 7 | align-items: center; 8 | padding: 5px; 9 | margin-bottom: 5px; 10 | } 11 | 12 | .editable-block__drag-handle { 13 | grid-area: 1 / 1; 14 | text-align: center; 15 | } 16 | 17 | .editable-block__top-container { 18 | grid-area: 1 / 2; 19 | width: 100%; 20 | display: flex; 21 | justify-content: flex-end; 22 | gap: 5px; 23 | } 24 | 25 | .editable-block__name { 26 | font-weight: bold; 27 | font-style: italic; 28 | width: 100%; 29 | } 30 | 31 | .editable-block__name_no-italic { 32 | font-style: normal; 33 | } 34 | 35 | .editable-block__button-separator { 36 | border-left: 1px solid gray; 37 | margin: 0 5px; 38 | } 39 | 40 | .editable-block__sidebar { 41 | grid-area: 2 / 1; 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: space-between; 45 | height: 100%; 46 | } 47 | 48 | .editable-block__expression-menu { 49 | flex: 1 1 auto; 50 | } 51 | 52 | .editable-block__textarea { 53 | grid-area: 2 / 2; 54 | font-family: monospace; 55 | resize: none; 56 | } 57 | 58 | .editable-block__preview-container { 59 | grid-area: 3 / 1 / 4 / 3; 60 | margin: 0; 61 | } 62 | 63 | .editable-block__preview_hanging-indent { 64 | padding-left: 1em; 65 | text-indent: -1em; 66 | } 67 | 68 | .editable-block__preview-name { 69 | margin: 0; 70 | display: inline; 71 | font-weight: bold; 72 | font-style: italic; 73 | } 74 | 75 | .editable-block__preview-name_no-italic { 76 | font-style: normal; 77 | } 78 | 79 | .editable-block__preview-text { 80 | display: inline; 81 | text-indent: 0; 82 | margin: 0; 83 | white-space: pre-wrap; 84 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/legendary-action-display-block-list.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/lists/legendary-action-display-block-list.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/legendary-action-display-block.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin-top: 0.3em; 3 | margin-bottom: 0.9em; 4 | } 5 | 6 | .legendary-action-display-block { 7 | display: block; 8 | line-height: 1.5; 9 | 10 | /* No vertical spacing for legendary action blocks. */ 11 | margin-top: 0; 12 | margin-bottom: 0; 13 | 14 | /* Hanging indent for legendary action blocks. */ 15 | padding-left: 1em; 16 | text-indent: -1em; 17 | } 18 | 19 | .legendary-action-display-block__name { 20 | margin: 0; 21 | display: inline; 22 | font-weight: bold; 23 | 24 | /* Non-italic name for legendary action blocks. */ 25 | font-style: normal; 26 | } 27 | 28 | .legendary-action-display-block__text { 29 | display: inline; 30 | text-indent: 0; 31 | margin: 0; 32 | white-space: pre-wrap; 33 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/legendary-action-editable-block-list.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/lists/legendary-action-editable-block-list.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/legendary-action-editable-block.css: -------------------------------------------------------------------------------- 1 | .legendary-action-editable-block__name { 2 | /* Non-italic name for legendary action blocks. */ 3 | font-style: normal; 4 | } 5 | 6 | .legendary-action-editable-block__preview-container { 7 | /* Hanging indent for legendary action blocks. */ 8 | padding-left: 1em; 9 | text-indent: -1em; 10 | } 11 | 12 | .legendary-action-editable-block__preview-name { 13 | /* Non-italic name for legendary action blocks. */ 14 | font-style: normal; 15 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/property-list-item.css: -------------------------------------------------------------------------------- 1 | .property-list-item__container { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | padding: 4px; 6 | } 7 | 8 | .property-list-item__label { 9 | text-align: center; 10 | } 11 | 12 | .property-list-item__remove-button { 13 | padding: 0 12px; 14 | } 15 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/lists/property-list.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .property-list__input-container { 7 | display: flex; 8 | flex-direction: row; 9 | align-items: center; 10 | margin: 0 5px 5px 5px; 11 | } 12 | 13 | .property-list__input { 14 | flex: 1 1 auto; 15 | } 16 | 17 | .property-list__add-button { 18 | padding: 0 12px; 19 | margin-left: 5px; 20 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/loading-screen.css: -------------------------------------------------------------------------------- 1 | .loading-screen { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | z-index: 100; 10 | width: 100vw; 11 | height: 100vh; 12 | background-color: white; 13 | opacity: 1; 14 | } 15 | 16 | .loading-screen_hidden { 17 | opacity: 0; 18 | visibility: hidden; 19 | height: 0; 20 | 21 | animation: hide-loading-screen 1s linear; 22 | } 23 | 24 | .loading-screen__title { 25 | margin-top: 0; 26 | margin-bottom: 50px; 27 | } 28 | 29 | .loading-screen__animation { 30 | border: 20px solid lightgray; 31 | border-top: 20px solid #7A200D; 32 | border-bottom: 20px solid #7A200D; 33 | border-radius: 50%; 34 | width: 120px; 35 | height: 120px; 36 | animation: spin 2s linear infinite; 37 | } 38 | 39 | .loading-screen__status { 40 | font-weight: bold; 41 | margin-top: 50px; 42 | } 43 | 44 | @keyframes hide-loading-screen { 45 | from { 46 | opacity: 1; 47 | visibility: visible; 48 | height: 100vh; 49 | } 50 | 51 | 50% { 52 | opacity: 1; 53 | visibility: visible; 54 | height: 100vh; 55 | } 56 | 57 | to { 58 | opacity: 0; 59 | visibility: hidden; 60 | height: 100vh; 61 | } 62 | } 63 | 64 | @keyframes spin { 65 | from { transform: rotate(0deg); } 66 | to { transform: rotate(360deg); } 67 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/menus/drop-down-menu.css: -------------------------------------------------------------------------------- 1 | .drop-down-menu:hover { 2 | background: #909090; 3 | } 4 | 5 | .drop-down-menu__icon-container { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: end; 10 | padding-left: 5px; 11 | padding-right: 5px; 12 | } 13 | 14 | .drop-down-menu__label { 15 | color: white; 16 | font-weight: bold; 17 | } 18 | 19 | .drop-down-menu__icon { 20 | color: white; 21 | cursor: default; 22 | } 23 | 24 | .drop-down-menu__items { 25 | display: flex; 26 | flex-direction: column; 27 | position: absolute; 28 | z-index: 2; 29 | visibility: hidden; 30 | background: #606060; 31 | color: white; 32 | border: 1px solid #909090; 33 | } 34 | 35 | .drop-down-menu:hover .drop-down-menu__items { 36 | visibility: visible; 37 | } 38 | 39 | ::slotted(button) { 40 | display: flex; 41 | justify-content: flex-start; 42 | background: #606060; 43 | border: 1px solid #909090; 44 | color: white; 45 | font-family: inherit; 46 | font-size: inherit; 47 | font-weight: bold; 48 | padding: 5px; 49 | cursor: pointer; 50 | transition: background 100ms ease-in-out; 51 | } 52 | 53 | ::slotted(button:hover) { 54 | background: #909090; 55 | } 56 | 57 | ::slotted(button:active) { 58 | background: #90FF70; 59 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/menus/expression-menu.css: -------------------------------------------------------------------------------- 1 | .expression-menu { 2 | position: relative; 3 | height: 100%; 4 | } 5 | 6 | .expression-menu__button { 7 | display: inline-flex; 8 | align-items: center; 9 | justify-content: center; 10 | background: #dddddd; 11 | border: 1px outset #949494; 12 | font-family: inherit; 13 | font-size: x-small; 14 | font-weight: bold; 15 | width: 100%; 16 | height: 100%; 17 | padding: 0; 18 | transition: background 100ms ease-in-out; 19 | } 20 | 21 | .expression-menu__button:hover { 22 | background: #eeeeee; 23 | } 24 | 25 | .expression-menu__items { 26 | display: flex; 27 | flex-direction: column; 28 | position: absolute; 29 | left: 34px; 30 | top: 0px; 31 | z-index: 2; 32 | visibility: hidden; 33 | background: #ffffaa; 34 | color: black; 35 | border: 1px solid #909090; 36 | } 37 | 38 | .expression-menu:hover .expression-menu__items { 39 | visibility: visible; 40 | } 41 | 42 | ::slotted(button) { 43 | display: flex; 44 | justify-content: flex-start; 45 | background: #ffffaa; 46 | color: black; 47 | border: 1px solid #909090; 48 | font-family: inherit; 49 | font-size: x-small; 50 | font-weight: bold; 51 | white-space: nowrap; 52 | cursor: pointer; 53 | transition: background 100ms ease-in-out; 54 | } 55 | 56 | ::slotted(button:hover) { 57 | background: #ffff00; 58 | } 59 | 60 | ::slotted(button:active) { 61 | background: #90FF70; 62 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/property-block.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin-top: 0.3em; 3 | margin-bottom: 0.9em; 4 | line-height: 1.5; 5 | display: block; 6 | } 7 | 8 | ::slotted(h4) { 9 | margin: 0; 10 | display: inline; 11 | font-weight: bold; 12 | font-style: italic; 13 | } 14 | 15 | ::slotted(p:first-of-type) { 16 | display: inline; 17 | text-indent: 0; 18 | } 19 | 20 | ::slotted(p) { 21 | text-indent: 1em; 22 | margin: 0; 23 | } 24 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/property-line.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | line-height: 1.4; 4 | text-indent: -1em; 5 | padding-left: 1em; 6 | } 7 | 8 | ::slotted(h4) { 9 | display: inline; 10 | font-weight: bold; 11 | margin: 0; 12 | } 13 | 14 | ::slotted(p:first-of-type) { 15 | display: inline; 16 | text-indent: 0; 17 | } 18 | 19 | ::slotted(p) { 20 | text-indent: 1em; 21 | margin: 0; 22 | } 23 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/section-divider.css: -------------------------------------------------------------------------------- 1 | .section-divider { 2 | opacity: 1; 3 | max-height: 100%; 4 | overflow: visible; 5 | visibility: visible; 6 | 7 | animation: show-section var(--section-animation-duration) ease-out; 8 | } 9 | 10 | .section-divider_hidden { 11 | opacity: 0; 12 | max-height: 0; 13 | overflow: hidden; 14 | visibility: hidden; 15 | 16 | animation: hide-section var(--section-animation-duration) ease-in; 17 | } 18 | 19 | .section-divider__line { 20 | fill: Gray; 21 | stroke: Gray; 22 | margin-top: 0.6em; 23 | margin-bottom: 0.35em; 24 | } 25 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/ability-scores-section.css: -------------------------------------------------------------------------------- 1 | .ability-scores-show-section { 2 | display: grid; 3 | grid-template-columns: auto auto auto auto auto auto; 4 | grid-template-rows: auto auto; 5 | grid-column-gap: 5px; 6 | justify-items: center; 7 | align-items: center; 8 | } 9 | 10 | .ability-scores-edit-section { 11 | display: grid; 12 | grid-template-columns: auto auto auto auto auto auto; 13 | grid-template-rows: auto auto; 14 | grid-column-gap: 5px; 15 | justify-items: center; 16 | align-items: center; 17 | } 18 | 19 | .ability-scores-section__score-label { 20 | font-weight: bold; 21 | cursor: inherit; 22 | } 23 | 24 | .ability-scores-section__strength-label { grid-area: 1 / 1; } 25 | .ability-scores-section__dexterity-label { grid-area: 1 / 2; } 26 | .ability-scores-section__constitution-label { grid-area: 1 / 3; } 27 | .ability-scores-section__intelligence-label { grid-area: 1 / 4; } 28 | .ability-scores-section__wisdom-label { grid-area: 1 / 5; } 29 | .ability-scores-section__charisma-label { grid-area: 1 / 6; } 30 | 31 | .ability-scores-show-section__score-values { 32 | display: flex; 33 | flex-wrap: wrap; 34 | justify-content: center; 35 | cursor: inherit; 36 | } 37 | 38 | .ability-scores-show-section__score-show { 39 | margin-right: 0.33em; 40 | } 41 | 42 | .ability-scores-edit-section__score-values { 43 | display: flex; 44 | flex-wrap: wrap; 45 | justify-content: center; 46 | width: 40px; 47 | } 48 | 49 | .ability-scores-section__strength-values { grid-area: 2 / 1; } 50 | .ability-scores-section__dexterity-values { grid-area: 2 / 2; } 51 | .ability-scores-section__constitution-values { grid-area: 2 / 3; } 52 | .ability-scores-section__intelligence-values { grid-area: 2 / 4; } 53 | .ability-scores-section__wisdom-values { grid-area: 2 / 5; } 54 | .ability-scores-section__charisma-values { grid-area: 2 / 6; } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/actions-section.css: -------------------------------------------------------------------------------- 1 | .special-traits-section__generate-attack-button { 2 | flex: 1 1 auto; 3 | padding: 5px; 4 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/armor-class-section.css: -------------------------------------------------------------------------------- 1 | .armor-class-show-section { 2 | min-width: 0; 3 | } 4 | 5 | .armor-class-edit-section { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .armor-class-edit-section__armor-class-container { 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | } 15 | 16 | .armor-class-edit-section__armor-class-label { 17 | flex: 0 0 95px; 18 | } 19 | 20 | .armor-class-edit-section__armor-type-container { 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | margin-top: 5px; 25 | } 26 | 27 | .armor-class-edit-section__armor-type-label { 28 | flex: 0 0 95px; 29 | } 30 | 31 | .armor-class-edit-section__armor-type-input { 32 | flex: 1 1 auto; 33 | } 34 | 35 | .armor-class-edit-section__shield-container { 36 | flex: 1 1 auto; 37 | display: flex; 38 | align-items: center; 39 | justify-content: space-evenly; 40 | } 41 | 42 | .armor-class-edit-section__custom-container { 43 | display: grid; 44 | grid-template-columns: 90px auto; 45 | grid-template-rows: auto auto; 46 | grid-column-gap: 5px; 47 | margin-top: 5px; 48 | } 49 | 50 | .armor-class-edit-section__use-custom-text-input { 51 | grid-area: 1 / 1; 52 | justify-self: end; 53 | } 54 | .armor-class-edit-section__use-use-custom-text-label { grid-area: 1 / 2; } 55 | .armor-class-edit-section__custom-text-help-tooltip { 56 | grid-area: 2 / 1; 57 | justify-self: end; 58 | align-self: end; 59 | } 60 | .armor-class-edit-section__custom-text-input { grid-area: 2 / 2; } 61 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/block-list-section.css: -------------------------------------------------------------------------------- 1 | .block-list-section__heading { 2 | border-bottom: 1px solid #7A200D; 3 | color: #7A200D; 4 | font-size: 21px; 5 | font-variant: small-caps; 6 | font-weight: normal; 7 | letter-spacing: 1px; 8 | overflow: hidden; 9 | 10 | break-inside: avoid-column; 11 | break-after: avoid-column; 12 | 13 | opacity: 1; 14 | max-height: 100%; 15 | margin: 0.3em 0; 16 | visibility: visible; 17 | 18 | animation: show-block-section-heading var(--section-animation-duration) ease-out; 19 | } 20 | 21 | .block-list-section__heading_hidden { 22 | opacity: 0; 23 | max-height: 0; 24 | margin: 0; 25 | visibility: hidden; 26 | 27 | animation: hide-block-section-heading var(--section-animation-duration) ease-in; 28 | } 29 | 30 | .block-list-section__empty-label { 31 | font-weight: bold; 32 | cursor: pointer; 33 | } 34 | 35 | .block-list-section__empty-label_hidden { 36 | display: none; 37 | } 38 | 39 | .block-list-section__button-container { 40 | display: flex; 41 | flex-direction: row; 42 | } 43 | 44 | .block-list-section__block-help-tooltip { 45 | align-self: center; 46 | margin-right: 5px; 47 | } 48 | 49 | .block-list-section__add-button { 50 | flex: 1 1 auto; 51 | padding: 5px; 52 | } 53 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/challenge-rating-section.css: -------------------------------------------------------------------------------- 1 | .challenge-rating-edit-section { 2 | display: grid; 3 | grid-template-columns: auto 50px auto auto 70px; 4 | grid-template-rows: auto auto; 5 | grid-row-gap: 5px; 6 | align-items: center; 7 | } 8 | 9 | .challenge-rating-edit-section__challenge-rating-label-container { 10 | grid-area: 1 / 1 / 3 / 2; 11 | } 12 | 13 | .challenge-rating-edit-section__challenge-rating-input { 14 | grid-area: 1 / 2 / 3 / 3; 15 | } 16 | 17 | .challenge-rating-edit-section__arrow { 18 | grid-area: 1 / 3 / 3 / 4; 19 | text-align: center; 20 | } 21 | 22 | .challenge-rating-edit-section__experience-points-label { 23 | grid-area: 1 / 4; 24 | } 25 | 26 | .challenge-rating-edit-section__experience-points-input { 27 | grid-area: 1 / 5; 28 | } 29 | 30 | .challenge-rating-edit-section__proficiency-bonus-label { 31 | grid-area: 2 / 4; 32 | } 33 | 34 | .challenge-rating-edit-section__proficiency-bonus-input { 35 | grid-area: 2 / 5; 36 | } 37 | 38 | .challenge-rating-edit-section__challenge-rating-help-tooltip { 39 | --help-tooltip-icon-color: #7A200D; 40 | --help-tooltip-width: 40em; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/condition-immunities-section.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/sections/condition-immunities-section.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/damage-immunities-section.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/sections/damage-immunities-section.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/damage-resistances-section.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/sections/damage-resistances-section.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/damage-vulnerabilities-section.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/sections/damage-vulnerabilities-section.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/hit-points-section.css: -------------------------------------------------------------------------------- 1 | .hit-points-edit-section { 2 | display: grid; 3 | grid-template-columns: 90px auto; 4 | grid-template-rows: auto 5px auto auto; 5 | grid-column-gap: 5px; 6 | align-items: center; 7 | } 8 | 9 | .hit-points-edit-section__hit-points-label { 10 | grid-area: 1 / 1; 11 | } 12 | 13 | .hit-points-edit-section__hit-points-input { 14 | grid-area: 1 / 2; 15 | } 16 | 17 | .hit-points-edit-section__use-hit-die-input { 18 | grid-area: 3 / 1; 19 | justify-self: end; 20 | } 21 | 22 | .hit-points-edit-section__use-hit-die-label { 23 | grid-area: 3 / 2; 24 | } 25 | 26 | .hit-points-edit-section__help-tooltip { 27 | grid-area: 4 / 1; 28 | justify-self: end; 29 | align-self: end; 30 | --help-tooltip-icon-color: #7A200D; 31 | --help-tooltip-width: 40em; 32 | } 33 | 34 | .hit-points-edit-section__help-tooltip-table-container { 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: center; 38 | } 39 | 40 | .hit-points-edit-section__help-tooltip-table-container table { 41 | background: white; 42 | border: 2px solid #aaaaaa; 43 | } 44 | 45 | .hit-points-edit-section__help-tooltip-table-container th { 46 | text-decoration: underline; 47 | } 48 | 49 | .hit-points-edit-section__help-tooltip-table-container th, td { 50 | text-align: center; 51 | padding-left: 10px; 52 | padding-right: 10px; 53 | } 54 | 55 | .hit-points-edit-section__hit-die-calculation-container { 56 | grid-area: 4 / 2; 57 | display: grid; 58 | grid-template-columns: auto auto auto 60px auto; 59 | grid-template-rows: auto auto; 60 | grid-column-gap: 5px; 61 | justify-items: center; 62 | align-items: center; 63 | border: 1px solid Gray; 64 | padding: 5px; 65 | } 66 | 67 | .hit-points-edit-section__hit-die-quantity-label { grid-area: 1 / 2; } 68 | .hit-points-edit-section__hit-die-size-label { grid-area: 1 / 3; } 69 | .hit-points-edit-section__constitution-hit-points-label { grid-area: 1 / 4; } 70 | 71 | .hit-points-edit-section__hit_die-leading-text-label { grid-area: 2 / 1; } 72 | .hit-points-edit-section__hit-die-quantity-input { grid-area: 2 / 2; } 73 | .hit-points-edit-section__hit-die-size-input { grid-area: 2 / 3; } 74 | .hit-points-edit-section__constitution-hit-points-value-label { grid-area: 2 / 4; } 75 | .hit-points-edit-section__hit-die-trailing-text-label { grid-area: 2 / 5; } 76 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/languages-section.css: -------------------------------------------------------------------------------- 1 | .languages-section__telepathy-container { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-evenly; 5 | align-items: center; 6 | margin-top: 5px; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/legendary-actions-section.css: -------------------------------------------------------------------------------- 1 | .legendary-actions-edit-section { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .legendary-actions-show-section__text { 7 | margin-top: 0.3em; 8 | margin-bottom: 0.9em; 9 | line-height: 1.5; 10 | } 11 | 12 | .legendary-actions-show-section__text_hidden { 13 | display: none; 14 | } 15 | 16 | .legendary-actions-edit-textarea-container { 17 | margin-right: 5px; 18 | } 19 | 20 | .legendary-actions-edit-textarea { 21 | width: 100%; 22 | font-family: monospace; 23 | resize: none; 24 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/property-list-section.css: -------------------------------------------------------------------------------- 1 | .property-list-edit-section { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .property-list-edit-section__heading-label { 7 | flex: 0 0 110px; 8 | } 9 | 10 | .property-list-edit-section__content { 11 | flex: 1 1 auto; 12 | display: flex; 13 | flex-direction: column; 14 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/reactions-section.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frumple/statblock5e-creator/d138d8e0f735f22e25e0e6a80cb474fc51e51639/src/css/elements/autonomous/sections/reactions-section.css -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/section.css: -------------------------------------------------------------------------------- 1 | .section { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | align-items: stretch; 6 | position: relative; 7 | 8 | opacity: 1; 9 | max-height: 100%; 10 | overflow: visible; 11 | visibility: visible; 12 | 13 | animation: show-section var(--section-animation-duration) ease-out; 14 | } 15 | 16 | .section_hidden { 17 | opacity: 0; 18 | max-height: 0; 19 | overflow: hidden; 20 | visibility: hidden; 21 | 22 | animation: hide-section var(--section-animation-duration) ease-in; 23 | } 24 | 25 | .section_selectable { 26 | cursor: pointer; 27 | } 28 | 29 | .section_selectable:hover { 30 | background: #FFCCCC; 31 | } 32 | 33 | .section_selectable:active { 34 | background: #FFDDDD; 35 | } 36 | 37 | .section_selectable:hover .section__edit-label { 38 | display: inline-flex; 39 | } 40 | 41 | .section_empty { 42 | color: Gray; 43 | } 44 | 45 | .section__content { 46 | flex: 1 1 auto; 47 | align-self: center; 48 | } 49 | 50 | .section__heading-label { 51 | font-weight: bold; 52 | } 53 | 54 | .section__save-button-container { 55 | display: flex; 56 | flex-direction: column; 57 | justify-content: center; 58 | } 59 | 60 | .section__save-button { 61 | flex: 1 1 auto; 62 | position: absolute; 63 | left: 410px; 64 | width: 25px; 65 | height: 100%; 66 | border-radius: 0 10px 10px 0; 67 | z-index: 1; 68 | } 69 | 70 | .section__edit-label-container { 71 | display: flex; 72 | flex-direction: column; 73 | justify-content: center; 74 | } 75 | 76 | .section__edit-label { 77 | position: absolute; 78 | left: 410px; 79 | color: #7A200D; 80 | z-index: 1; 81 | } 82 | 83 | .section__edit-label_hidden { 84 | display: none; 85 | } 86 | 87 | .section__error-highlight { 88 | border: 2px solid red; 89 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/senses-section.css: -------------------------------------------------------------------------------- 1 | .senses-show-section { 2 | min-width: 0; 3 | } 4 | 5 | .senses-edit-section { 6 | display: grid; 7 | grid-template-columns: 60px auto auto auto auto; 8 | grid-template-rows: auto auto auto auto 5px auto 5px auto auto; 9 | grid-column-gap: 5px; 10 | align-items: center; 11 | } 12 | 13 | .senses-edit-section__senses-label { grid-area: 1 / 1; } 14 | 15 | .senses-edit-section__blindsight-label { grid-area: 1 / 2; } 16 | .senses-edit-section__blindsight-input { grid-area: 1 / 3; } 17 | .senses-edit-section__blindsight-unit { grid-area: 1 / 4; } 18 | .senses-edit-section__blind-beyond-this-radius-container { 19 | grid-area: 1 / 5 / 3 / 6; 20 | align-self: start; 21 | display: flex; 22 | margin-top: 3px; 23 | } 24 | 25 | .senses-edit-section__darkvision-label { grid-area: 2 / 2; } 26 | .senses-edit-section__darkvision-input { grid-area: 2 / 3; } 27 | .senses-edit-section__darkvision-unit { grid-area: 2 / 4; } 28 | 29 | .senses-edit-section__tremorsense-label { grid-area: 3 / 2; } 30 | .senses-edit-section__tremorsense-input { grid-area: 3 / 3; } 31 | .senses-edit-section__tremorsense-unit { grid-area: 3 / 4; } 32 | 33 | .senses-edit-section__truesight-label { grid-area: 4 / 2; } 34 | .senses-edit-section__truesight-input { grid-area: 4 / 3; } 35 | .senses-edit-section__truesight-unit { grid-area: 4 / 4; } 36 | 37 | .senses-edit-section__passive-perception-help-tooltip { 38 | grid-area: 6 / 1; 39 | justify-self: end; 40 | align-self: center; 41 | --help-tooltip-icon-color: #7A200D; 42 | --help-tooltip-width: 50em; 43 | } 44 | 45 | .senses-edit-section__passive-perception-label { grid-area: 6 / 2; } 46 | .senses-edit-section__passive-perception-value { grid-area: 6 / 3; } 47 | 48 | .senses-edit-section__use-custom-text-input { 49 | grid-area: 8 / 1; 50 | justify-self: end; 51 | } 52 | .senses-edit-section__use-custom-text-label { grid-area: 8 / 2 / 9 / 6; } 53 | .senses-edit-section__custom-text-help-tooltip { 54 | grid-area: 9 / 1; 55 | justify-self: end; 56 | align-self: end; 57 | } 58 | .senses-edit-section__custom-text-input { grid-area: 9 / 2 / 10 / 6; } 59 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/special-traits-section.css: -------------------------------------------------------------------------------- 1 | .special-traits-section__heading { 2 | display: none; 3 | } 4 | 5 | .special-traits-section__generate-spellcasting-button { 6 | flex: 1 1 auto; 7 | padding: 5px; 8 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/speed-section.css: -------------------------------------------------------------------------------- 1 | .speed-show-section { 2 | min-width: 0; 3 | } 4 | 5 | .speed-edit-section { 6 | display: grid; 7 | grid-template-columns: 90px auto auto auto auto; 8 | grid-template-rows: auto auto auto auto auto 5px auto auto; 9 | grid-column-gap: 5px; 10 | align-items: center; 11 | } 12 | 13 | .speed-edit-section__speed-label { grid-area: 1 / 1; } 14 | 15 | .speed-edit-section__walk-label { grid-area: 1 / 2; } 16 | .speed-edit-section__walk-input { grid-area: 1 / 3; } 17 | .speed-edit-section__walk-unit { grid-area: 1 / 4; } 18 | 19 | .speed-edit-section__burrow-label { grid-area: 2 / 2; } 20 | .speed-edit-section__burrow-input { grid-area: 2 / 3; } 21 | .speed-edit-section__burrow-unit { grid-area: 2 / 4; } 22 | 23 | .speed-edit-section__climb-label { grid-area: 3 / 2; } 24 | .speed-edit-section__climb-input { grid-area: 3 / 3; } 25 | .speed-edit-section__climb-unit { grid-area: 3 / 4; } 26 | 27 | .speed-edit-section__fly-label { grid-area: 4 / 2; } 28 | .speed-edit-section__fly-input { grid-area: 4 / 3; } 29 | .speed-edit-section__fly-unit { grid-area: 4 / 4; } 30 | .speed-edit-section__hover-container { 31 | grid-area: 4 / 5; 32 | display: flex; 33 | align-items: center; 34 | justify-content: space-evenly; 35 | } 36 | 37 | .speed-edit-section__swim-label { grid-area: 5 / 2; } 38 | .speed-edit-section__swim-input { grid-area: 5 / 3; } 39 | .speed-edit-section__swim-unit { grid-area: 5 / 4; } 40 | 41 | .speed-edit-section__use-custom-text-input { 42 | grid-area: 7 / 1; 43 | justify-self: end; 44 | } 45 | .speed-edit-section__use-custom-text-label { grid-area: 7 / 2 / 8 / 6; } 46 | .speed-edit-section__custom-text-help-tooltip { 47 | grid-area: 8 / 1; 48 | justify-self: end; 49 | align-self: end; 50 | } 51 | .speed-edit-section__custom-text-input { grid-area: 8 / 2 / 9 / 6; } 52 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/subtitle-section.css: -------------------------------------------------------------------------------- 1 | .subsubtitle-show-section { 2 | min-width: 0; 3 | } 4 | 5 | .subtitle-show-section__subtitle-text { 6 | font-weight: normal; 7 | font-style: italic; 8 | font-size: 12px; 9 | margin: 0; 10 | } 11 | 12 | .subtitle-edit-section { 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .subtitle-edit-section__label { 18 | color: #7A200D; 19 | } 20 | 21 | .subtitle-edit-section__heading-label { 22 | font-weight: bold; 23 | } 24 | 25 | .subtitle-edit-section__subtitle-container { 26 | display: grid; 27 | grid-template-columns: auto auto minmax(0, 1fr) auto; 28 | grid-template-rows: auto auto; 29 | grid-column-gap: 5px; 30 | margin-top: 5px; 31 | } 32 | 33 | .subtitle-edit-section__size-label-container { 34 | grid-area: 1 / 1; 35 | display: flex; 36 | flex-direction: row; 37 | } 38 | 39 | .subtitle-edit-section__size-help-tooltip { 40 | --help-tooltip-icon-color: #7A200D; 41 | --help-tooltip-width: 30em; 42 | margin-left: 5px; 43 | } 44 | 45 | .subtitle-edit-section__type-label { grid-area: 1 / 2; } 46 | .subtitle-edit-section__tags-label { grid-area: 1 / 3; } 47 | .subtitle-edit-section__alignment-label { grid-area: 1 / 4; } 48 | 49 | .subtitle-edit-section__size-input { grid-area: 2 / 1; } 50 | .subtitle-edit-section__type-input { grid-area: 2 / 2; } 51 | .subtitle-edit-section__tags-input { grid-area: 2 / 3; } 52 | .subtitle-edit-section__alignment-input { grid-area: 2 / 4; } 53 | 54 | .subtitle-edit-section__custom-container { 55 | display: grid; 56 | grid-template-columns: auto minmax(0, 1fr); 57 | grid-template-rows: auto auto; 58 | grid-column-gap: 5px; 59 | margin-top: 5px; 60 | } 61 | 62 | .subtitle-edit-section__use-custom-text-input { 63 | grid-area: 1 / 1; 64 | justify-self: end; 65 | } 66 | .subtitle-edit-section__use-use-custom-text-label { grid-area: 1 / 2; } 67 | .subtitle-edit-section__custom-text-input { grid-area: 2 / 2; } -------------------------------------------------------------------------------- /src/css/elements/autonomous/sections/title-section.css: -------------------------------------------------------------------------------- 1 | .title-show-section { 2 | min-width: 0; 3 | } 4 | 5 | .title-show-section__title-text { 6 | font-family: 'Libre Baskerville', 'Lora', 'Calisto MT', 7 | 'Bookman Old Style', Bookman, 'Goudy Old Style', 8 | Garamond, 'Hoefler Text', 'Bitstream Charter', 9 | Georgia, serif; 10 | color: #7A200D; 11 | font-size: 23px; 12 | font-weight: 700; 13 | font-variant: small-caps; 14 | letter-spacing: 1px; 15 | margin: 0; 16 | } 17 | 18 | .title-edit-section { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .title-edit-section__label { 24 | color: #7A200D; 25 | } 26 | 27 | .title-edit-section__title-input { 28 | height: 30px; 29 | } 30 | 31 | .title-edit-section__meta-container { 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | margin-top: 5px; 36 | } 37 | 38 | .title-edit-section__short-name-help-tooltip { 39 | --help-tooltip-icon-color: #7A200D; 40 | --help-tooltip-width: 35em; 41 | margin-left: 5px; 42 | } 43 | 44 | .title-edit-section__short-name-help-tooltip code { 45 | background: white; 46 | } 47 | 48 | .title-edit-section__short-name-help-tooltip-table-container { 49 | display: flex; 50 | flex-direction: row; 51 | justify-content: center; 52 | } 53 | 54 | .title-edit-section__short-name-help-tooltip-table-container table { 55 | background: white; 56 | border: 2px solid #aaaaaa; 57 | } 58 | 59 | .title-edit-section__short-name-help-tooltip-table-container th { 60 | text-decoration: underline; 61 | } 62 | 63 | .title-edit-section__short-name-help-tooltip-table-container th, td { 64 | text-align: center; 65 | padding-left: 10px; 66 | padding-right: 10px; 67 | } 68 | 69 | .title-edit-section__short-name-input { 70 | min-width: 0; 71 | flex: 1 1 5em; 72 | margin-left: 5px; 73 | } 74 | 75 | .title-edit-section__proper-noun-input { 76 | margin-left: 10px; 77 | } 78 | 79 | .title-edit-section__proper-noun-label { 80 | margin-left: 5px; 81 | } 82 | 83 | .title-edit-section__proper-noun-help-tooltip { 84 | --help-tooltip-icon-color: #7A200D; 85 | --help-tooltip-width: 30em; 86 | margin-left: 5px; 87 | } 88 | 89 | .title-edit-section__proper-noun-help-tooltip code { 90 | background: white; 91 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/spell-category-box.css: -------------------------------------------------------------------------------- 1 | .spell-category__container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | border: 1px solid #aaaaaa; 6 | width: 228px; 7 | padding: 5px; 8 | margin: 5px; 9 | } 10 | 11 | .spell-category__container_disabled { 12 | display: none; 13 | background: #eeeeee; 14 | } 15 | 16 | .spell-category__heading { 17 | font-weight: bold; 18 | font-style: italic; 19 | } 20 | 21 | .spell-category__property-list { 22 | margin: 5px 5px 0 5px; 23 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/tapered-rule.css: -------------------------------------------------------------------------------- 1 | svg { 2 | fill: #922610; 3 | /* Stroke is necessary for good antialiasing in Chrome. */ 4 | stroke: #922610; 5 | margin-top: 0.6em; 6 | margin-bottom: 0.35em; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/elements/autonomous/tooltips/custom-text-help-tooltip.css: -------------------------------------------------------------------------------- 1 | .custom-text-help-tooltip { 2 | --help-tooltip-icon-color: #7A200D; 3 | --help-tooltip-width: 40em; 4 | } 5 | 6 | table { 7 | margin-left: 2em; 8 | } 9 | 10 | code { 11 | background: white; 12 | border-radius: 3px; 13 | font-family: monospace; 14 | padding: 0 3px; 15 | } -------------------------------------------------------------------------------- /src/css/elements/autonomous/tooltips/help-tooltip.css: -------------------------------------------------------------------------------- 1 | .help-tooltip__icon-container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: end; 5 | } 6 | 7 | .help-tooltip__icon { 8 | color: var(--help-tooltip-icon-color); 9 | cursor: help; 10 | } 11 | 12 | .help-tooltip__text { 13 | position: absolute; 14 | z-index: 2; 15 | display: none; 16 | background: #ffffaa; 17 | color: black; 18 | border: 2px solid black; 19 | white-space: normal; 20 | width: var(--help-tooltip-width); 21 | padding: 5px; 22 | } 23 | 24 | .help-tooltip:hover .help-tooltip__text { 25 | display: block; 26 | } 27 | 28 | ::slotted(h4) { 29 | margin-top: 0; 30 | margin-bottom: 0; 31 | } 32 | 33 | ::slotted(p) { 34 | margin-top: 0; 35 | margin-bottom: 0; 36 | } 37 | 38 | ::slotted(ul) { 39 | margin-top: 0; 40 | margin-bottom: 0; 41 | } 42 | 43 | ::slotted(blockquote) { 44 | display: inline-block; 45 | background: white; 46 | border: 2px solid #aaaaaa; 47 | font-family: monospace; 48 | white-space: pre-wrap; 49 | margin: 0.5em 0 0.5em 1em; 50 | padding: 0.5em; 51 | } 52 | 53 | ::slotted(cite) { 54 | font-style: italic; 55 | } -------------------------------------------------------------------------------- /src/css/elements/ui-controls.css: -------------------------------------------------------------------------------- 1 | /* Button */ 2 | 3 | .ui__button { 4 | display: inline-flex; 5 | align-items: center; 6 | justify-content: center; 7 | background: #dddddd; 8 | border: 1px outset #949494; 9 | font-family: inherit; 10 | font-size: inherit; 11 | font-weight: bold; 12 | padding: 0; 13 | cursor: pointer; 14 | transition: background 100ms ease-in-out; 15 | } 16 | 17 | .ui__button:enabled:hover { 18 | background: #eeeeee; 19 | } 20 | 21 | .ui__button:enabled:active { 22 | background: #90FF70; 23 | } 24 | 25 | /* Toggle Button (Checkbox or Radio Button that looks like a Button) */ 26 | 27 | .ui__toggle-button-input { 28 | display: none; 29 | appearance: none; 30 | } 31 | 32 | .ui__toggle-button-input:disabled + .ui__toggle-button-label { 33 | color: rgba(16, 16, 16, 0.3); 34 | } 35 | 36 | .ui__toggle-button-input:enabled + .ui__toggle-button-label:hover { 37 | background: #eeeeee; 38 | } 39 | 40 | .ui__toggle-button-input:enabled + .ui__toggle-button-label:active { 41 | background: #90FF70; 42 | } 43 | 44 | .ui__toggle-button-input:checked + .ui__toggle-button-label, 45 | .ui__toggle-button-input:checked + .ui__toggle-button-label:hover { 46 | background: #90FF70; 47 | } 48 | 49 | .ui__toggle-button-label { 50 | user-select: none; 51 | } 52 | 53 | /* Text Input */ 54 | 55 | .ui__text-input { 56 | font-family: inherit; 57 | font-size: inherit; 58 | box-sizing: border-box; 59 | height: 26px; 60 | } 61 | 62 | /* Number Input */ 63 | 64 | .ui__number-input { 65 | font-family: inherit; 66 | font-size: inherit; 67 | box-sizing: border-box; 68 | height: 26px; 69 | } 70 | 71 | .ui__small-number-input { 72 | width: 50px; 73 | } 74 | 75 | /* Select */ 76 | 77 | .ui__select { 78 | font-family: inherit; 79 | font-size: inherit; 80 | box-sizing: border-box; 81 | height: 26px; 82 | } 83 | 84 | /* Text Area */ 85 | 86 | .ui__textarea { 87 | font-family: inherit; 88 | font-size: inherit; 89 | } 90 | -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Noto Sans', 'Myriad Pro', Calibri, Helvetica, Arial, 3 | sans-serif; 4 | font-size: 13.5px; 5 | margin: 0; 6 | } 7 | 8 | .stat-block-editor { 9 | display: block; 10 | user-select: none; 11 | } 12 | 13 | .stat-block-editor_hidden { 14 | display: none; 15 | } 16 | -------------------------------------------------------------------------------- /src/css/section-animations.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --section-animation-duration: 500ms; 3 | } 4 | 5 | @keyframes show-section { 6 | from { 7 | opacity: 0; 8 | max-height: 0; 9 | overflow: hidden; 10 | visibility: hidden; 11 | } 12 | 13 | 50% { 14 | opacity: 0; 15 | max-height: 0; 16 | overflow: hidden; 17 | visibility: hidden; 18 | } 19 | } 20 | 21 | @keyframes hide-section { 22 | from { 23 | opacity: 1; 24 | max-height: 100%; 25 | overflow: visible; 26 | visibility: visible; 27 | } 28 | 29 | 50% { 30 | opacity: 0; 31 | max-height: 0; 32 | overflow: hidden; 33 | visibility: hidden; 34 | } 35 | } 36 | 37 | @keyframes show-block-section-heading { 38 | from { 39 | opacity: 0; 40 | max-height: 0; 41 | margin: 0; 42 | visibility: hidden; 43 | } 44 | 45 | 50% { 46 | opacity: 0; 47 | max-height: 0; 48 | margin: 0; 49 | visibility: hidden; 50 | } 51 | } 52 | 53 | @keyframes hide-block-section-heading { 54 | from { 55 | opacity: 1; 56 | max-height: 100%; 57 | margin: 0.3em 0; 58 | visibility: visible; 59 | } 60 | 61 | 50% { 62 | opacity: 0; 63 | max-height: 0; 64 | margin: 0; 65 | visibility: hidden; 66 | } 67 | } -------------------------------------------------------------------------------- /src/css/vendor/material-icons.css: -------------------------------------------------------------------------------- 1 | /* Rules for sizing the icon. */ 2 | .material-icons.md-18 { font-size: 18px; } 3 | .material-icons.md-24 { font-size: 24px; } 4 | .material-icons.md-36 { font-size: 36px; } 5 | .material-icons.md-48 { font-size: 48px; } 6 | 7 | /* Rules for using icons as black on a light background. */ 8 | .material-icons.md-dark { color: rgba(0, 0, 0, 0.54); } 9 | .material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); } 10 | 11 | /* Rules for using icons as white on a dark background. */ 12 | .material-icons.md-light { color: rgba(255, 255, 255, 1); } 13 | .material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); } 14 | 15 | .material-icons__align-middle { 16 | display: inline; 17 | vertical-align: middle; 18 | } -------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/advanced-stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/basic-stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/bottom-stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/heading-stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/stat-block-sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/stat-block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 | 14 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/containers/top-stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/custom-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/export-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |

7 | 8 |
9 | 10 |
11 |

12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/import-dialog.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/import-json-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |
12 |

13 |
14 | publish 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/import-open5e-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |

7 | 8 |
9 | 10 |
11 |

12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/import-srd-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |

7 | 8 |
9 | 10 |
11 |

12 |
13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/option-dialog.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/dialogs/reset-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |

7 | 8 |
9 | 10 |
11 |

12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/error-messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 6 |
7 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/display-block-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/display-block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

.

5 |

6 |
7 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/drag-and-drop-list-item.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/editable-block-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/legendary-action-display-block-list.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/legendary-action-display-block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

.

5 |

6 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/legendary-action-editable-block-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/property-list-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | drag_handle 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/lists/property-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/loading-screen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Statblock5e Creator

5 |
6 | Defining elements... 7 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/menus/drop-down-menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/menus/expression-menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |
8 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/property-block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/property-line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/section-divider.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/actions-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Actions

4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/block-list-section.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/condition-immunities-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |

9 |

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/damage-immunities-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |

9 |

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/damage-resistances-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |

9 |

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/damage-vulnerabilities-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |

9 |

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/languages-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |

9 |

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/legendary-actions-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Legendary Actions

4 | 5 | 6 | 7 |
8 |
9 | 10 |

11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/property-list-section.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/reactions-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Reactions

4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/sections/special-traits-section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/slide-toggle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 9 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/spell-category-box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/tapered-rule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/html/elements/autonomous/tooltips/custom-text-help-tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

This custom text field supports Markdown Emphasis syntax:

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Italic:*single asterisks* or _single underscores_
Bold:**double asterisks** or __double underscores__
Italic+Bold:***triple asterisks*** or ___triple underscores___
19 |
-------------------------------------------------------------------------------- /src/html/elements/autonomous/tooltips/help-tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 | help 9 |
10 |
11 | 12 |
13 |
-------------------------------------------------------------------------------- /src/js/api/open5e-client.js: -------------------------------------------------------------------------------- 1 | export default class Open5eClient { 2 | constructor() { 3 | this.cachedCreatureLists = new Map(); 4 | } 5 | 6 | static getCreatureListUrl(documentSlug) { 7 | return `https://api.open5e.com/monsters/?format=json&fields=name,slug&limit=1000&document__slug=${documentSlug}`; 8 | } 9 | 10 | static getCreatureUrl(creatureSlug) { 11 | return `https://api.open5e.com/monsters/${creatureSlug}`; 12 | } 13 | 14 | async loadCreatureList(documentSlug) { 15 | if (this.cachedCreatureLists.has(documentSlug)) { 16 | return this.cachedCreatureLists.get(documentSlug); 17 | } 18 | 19 | const url = Open5eClient.getCreatureListUrl(documentSlug); 20 | const json = await fetch(url).then(response => response.json()); 21 | const results = json.results; 22 | 23 | this.cachedCreatureLists.set(documentSlug, results); 24 | return results; 25 | } 26 | 27 | async loadCreature(creatureSlug) { 28 | const url = Open5eClient.getCreatureUrl(creatureSlug); 29 | return await fetch(url).then(response => response.json()); 30 | } 31 | } -------------------------------------------------------------------------------- /src/js/data/challenge-rating-to-experience-points.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '0' : 10, 3 | '1/8' : 25, 4 | '1/4' : 50, 5 | '1/2' : 100, 6 | '1' : 200, 7 | '2' : 450, 8 | '3' : 700, 9 | '4' : 1100, 10 | '5' : 1800, 11 | '6' : 2300, 12 | '7' : 2900, 13 | '8' : 3900, 14 | '9' : 5000, 15 | '10' : 5900, 16 | '11' : 7200, 17 | '12' : 8400, 18 | '13' : 10000, 19 | '14' : 11500, 20 | '15' : 13000, 21 | '16' : 15000, 22 | '17' : 18000, 23 | '18' : 20000, 24 | '19' : 22000, 25 | '20' : 25000, 26 | '21' : 33000, 27 | '22' : 41000, 28 | '23' : 50000, 29 | '24' : 62000, 30 | '25' : 75000, 31 | '26' : 90000, 32 | '27' : 105000, 33 | '28' : 120000, 34 | '29' : 135000, 35 | '30' : 155000 36 | }; 37 | -------------------------------------------------------------------------------- /src/js/data/challenge-rating-to-proficiency-bonus.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '0' : 2, 3 | '1/8' : 2, 4 | '1/4' : 2, 5 | '1/2' : 2, 6 | '1' : 2, 7 | '2' : 2, 8 | '3' : 2, 9 | '4' : 2, 10 | '5' : 3, 11 | '6' : 3, 12 | '7' : 3, 13 | '8' : 3, 14 | '9' : 4, 15 | '10' : 4, 16 | '11' : 4, 17 | '12' : 4, 18 | '13' : 5, 19 | '14' : 5, 20 | '15' : 5, 21 | '16' : 5, 22 | '17' : 6, 23 | '18' : 6, 24 | '19' : 6, 25 | '20' : 6, 26 | '21' : 7, 27 | '22' : 7, 28 | '23' : 7, 29 | '24' : 7, 30 | '25' : 8, 31 | '26' : 8, 32 | '27' : 8, 33 | '28' : 8, 34 | '29' : 9, 35 | '30' : 9 36 | }; 37 | -------------------------------------------------------------------------------- /src/js/data/conditions.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'blinded', 3 | 'charmed', 4 | 'deafened', 5 | 'exhaustion', 6 | 'frightened', 7 | 'grappled', 8 | 'incapacitated', 9 | 'invisible', 10 | 'paralyzed', 11 | 'petrified', 12 | 'poisoned', 13 | 'prone', 14 | 'restrained', 15 | 'stunned', 16 | 'unconscious' 17 | ]; -------------------------------------------------------------------------------- /src/js/data/creature-alignments.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'unaligned', 3 | 'any alignment', 4 | 'lawful good', 5 | 'neutral good', 6 | 'chaotic good', 7 | 'lawful neutral', 8 | 'neutral', 9 | 'chaotic neutral', 10 | 'lawful evil', 11 | 'neutral evil', 12 | 'chaotic evil' 13 | ]; -------------------------------------------------------------------------------- /src/js/data/creature-sizes-to-hit-die-sizes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Tiny': 4, 3 | 'Small': 6, 4 | 'Medium': 8, 5 | 'Large': 10, 6 | 'Huge': 12, 7 | 'Gargantuan': 20 8 | }; -------------------------------------------------------------------------------- /src/js/data/creature-tags.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'aarakocra', 3 | 'aasimar', 4 | 'any race', 5 | 'bullywug', 6 | 'demon', 7 | 'devil', 8 | 'dragonborn', 9 | 'dwarf', 10 | 'elf', 11 | 'firbolg', 12 | 'gith', 13 | 'gnoll', 14 | 'gnome', 15 | 'goblinoid', 16 | 'goliath', 17 | 'grimlock', 18 | 'half-elf', 19 | 'halfling', 20 | 'half-orc', 21 | 'human', 22 | 'kenku', 23 | 'kobold', 24 | 'kuo-toa', 25 | 'lizardfolk', 26 | 'merfolk', 27 | 'orc', 28 | 'quaggoth', 29 | 'sahuagin', 30 | 'shapechanger', 31 | 'tabaxi', 32 | 'thri-kreen', 33 | 'tiefling', 34 | 'titan', 35 | 'triton', 36 | 'troglodyte', 37 | 'yuan-ti', 38 | 'yugoloth' 39 | ]; -------------------------------------------------------------------------------- /src/js/data/creature-types.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'aberration', 3 | 'beast', 4 | 'celestial', 5 | 'construct', 6 | 'dragon', 7 | 'elemental', 8 | 'fey', 9 | 'fiend', 10 | 'giant', 11 | 'humanoid', 12 | 'monstrosity', 13 | 'ooze', 14 | 'plant', 15 | 'undead' 16 | ]; -------------------------------------------------------------------------------- /src/js/data/damage-types-for-property-lists.js: -------------------------------------------------------------------------------- 1 | import DamageTypes from './damage-types.js'; 2 | 3 | // An array of damage types intended for the 4 | // Damage Vulnerabilities/Resistances/Immunities autocompletion. 5 | 6 | const damageTypes = DamageTypes.slice(); 7 | damageTypes.push('bludgeoning, piercing, and slashing from nonmagical attacks'); 8 | 9 | export default damageTypes; -------------------------------------------------------------------------------- /src/js/data/damage-types.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'acid', 3 | 'bludgeoning', 4 | 'cold', 5 | 'fire', 6 | 'force', 7 | 'lightning', 8 | 'necrotic', 9 | 'piercing', 10 | 'poison', 11 | 'psychic', 12 | 'radiant', 13 | 'slashing', 14 | 'thunder' 15 | ]; -------------------------------------------------------------------------------- /src/js/data/languages.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'Aarakocra', 3 | 'Abyssal', 4 | 'Aquan', 5 | 'Auran', 6 | 'Blink Dog', 7 | 'Bullywug', 8 | 'Celestial', 9 | 'Common', 10 | 'Deep Speech', 11 | 'Draconic', 12 | 'Druidic', 13 | 'Dwarvish', 14 | 'Elvish', 15 | 'Giant', 16 | 'Giant Eagle', 17 | 'Giant Elk', 18 | 'Giant Owl', 19 | 'Gith', 20 | 'Gnoll', 21 | 'Gnomish', 22 | 'Goblin', 23 | 'Grell', 24 | 'Grung', 25 | 'Halfling', 26 | 'Hook Horror', 27 | 'Ignan', 28 | 'Infernal', 29 | 'Kraul', 30 | 'Loxodon', 31 | 'Merfolk', 32 | 'Minotaur', 33 | 'Modron', 34 | 'Orc', 35 | 'Otyugh', 36 | 'Primordial', 37 | 'Sahuagin', 38 | 'Slaad', 39 | 'Sphinx', 40 | 'Sylvan', 41 | 'Terran', 42 | 'Thieves\' Cant', 43 | 'Thri-kreen', 44 | 'Tlincalli', 45 | 'Troglodyte', 46 | 'Undercommon', 47 | 'Vedalkin', 48 | 'Vegepygmy', 49 | 'Winter Wolf', 50 | 'Worg', 51 | 'Yeti' 52 | ]; -------------------------------------------------------------------------------- /src/js/elements/autonomous/containers/basic-stats.js: -------------------------------------------------------------------------------- 1 | import DivisibleContainer from './divisible-container.js'; 2 | 3 | export default class BasicStats extends DivisibleContainer { 4 | static get elementName() { return 'basic-stats'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'basic-stats', 8 | 'src/html/elements/autonomous/containers/basic-stats.html'); 9 | } 10 | 11 | constructor() { 12 | super(BasicStats.templatePaths); 13 | 14 | this.sections.set('armorClass', this.shadowRoot.querySelector('armor-class-section')); 15 | this.sections.set('hitPoints', this.shadowRoot.querySelector('hit-points-section')); 16 | this.sections.set('speed', this.shadowRoot.querySelector('speed-section')); 17 | } 18 | 19 | updateHitPointsView() { 20 | this.sections.get('hitPoints').updateView(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/containers/bottom-stats.js: -------------------------------------------------------------------------------- 1 | import StatsContainer from './stats-container.js'; 2 | 3 | export default class BottomStats extends StatsContainer { 4 | static get elementName() { return 'bottom-stats'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'bottom-stats', 8 | 'src/html/elements/autonomous/containers/bottom-stats.html'); 9 | } 10 | 11 | constructor() { 12 | super(BottomStats.templatePaths); 13 | 14 | this.sections.set('specialTraits', this.shadowRoot.querySelector('special-traits-section')); 15 | this.sections.set('actions', this.shadowRoot.querySelector('actions-section')); 16 | this.sections.set('reactions', this.shadowRoot.querySelector('reactions-section')); 17 | this.sections.set('legendaryActions', this.shadowRoot.querySelector('legendary-actions-section')); 18 | 19 | // console.log(this.sections); 20 | } 21 | 22 | reparseAllSections() { 23 | for (const section of this.sections.values()) { 24 | section.reparse(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/containers/divisible-container.js: -------------------------------------------------------------------------------- 1 | import StatsContainer from './stats-container.js'; 2 | 3 | export default class DivisibleContainer extends StatsContainer { 4 | constructor(templatePaths) { 5 | super(templatePaths); 6 | 7 | this.dividers = this.shadowRoot.querySelectorAll('section-divider'); 8 | 9 | this.addEventListener('sectionModeChanged', () => { 10 | this.updateSectionDividers(); 11 | }); 12 | } 13 | 14 | setEmptyVisibility(visibility) { 15 | super.setEmptyVisibility(visibility); 16 | 17 | this.updateSectionDividers(); 18 | } 19 | 20 | updateSectionDividers() { 21 | for (const divider of this.dividers) { 22 | divider.hidden = true; 23 | } 24 | 25 | let previousSection = null; 26 | 27 | for (const currentSection of this.sections.values()) { 28 | if (currentSection.mode !== 'hidden') { 29 | if (previousSection) { 30 | if (DivisibleContainer.shouldSectionDividerBeDisplayed(previousSection, currentSection)) { 31 | const previousDivider = currentSection.previousElementSibling; 32 | previousDivider.hidden = false; 33 | } 34 | } 35 | 36 | previousSection = currentSection; 37 | } 38 | } 39 | } 40 | 41 | static shouldSectionDividerBeDisplayed(sectionAbove, sectionBelow) { 42 | return (sectionAbove.mode === 'edit' && sectionBelow.mode !== 'hidden') || 43 | (sectionBelow.mode === 'edit' && sectionAbove.mode !== 'hidden'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/containers/heading-stats.js: -------------------------------------------------------------------------------- 1 | import DivisibleContainer from './divisible-container.js'; 2 | 3 | export default class HeadingStats extends DivisibleContainer { 4 | static get elementName() { return 'heading-stats'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'heading-stats', 8 | 'src/html/elements/autonomous/containers/heading-stats.html'); 9 | } 10 | 11 | constructor() { 12 | super(HeadingStats.templatePaths); 13 | 14 | this.sections.set('title', this.shadowRoot.querySelector('title-section')); 15 | this.sections.set('subtitle', this.shadowRoot.querySelector('subtitle-section')); 16 | } 17 | 18 | edit() { 19 | // Make sure title section gets the last focus 20 | this.sections.get('subtitle').edit(); 21 | this.sections.get('title').edit(); 22 | } 23 | 24 | exportToJson() { 25 | return { 26 | title: this.sections.get('title').exportToJson(), 27 | subtitle: this.sections.get('subtitle').exportToJson() 28 | }; 29 | } 30 | 31 | exportToHtml() { 32 | const creatureHeading = document.createElement('creature-heading'); 33 | const titleElement = this.sections.get('title').exportToHtml(); 34 | const subtitleElement = this.sections.get('subtitle').exportToHtml(); 35 | 36 | creatureHeading.appendChild(titleElement); 37 | creatureHeading.appendChild(subtitleElement); 38 | 39 | return creatureHeading; 40 | } 41 | 42 | exportToMarkdown() { 43 | return `${this.sections.get('title').exportToMarkdown()}\n${this.sections.get('subtitle').exportToMarkdown()}`; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/containers/stats-container.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class StatsContainer extends CustomAutonomousElement { 4 | constructor(templatePaths) { 5 | super(templatePaths); 6 | 7 | this.sections = new Map(); 8 | } 9 | 10 | setEmptyVisibility(visibility) { 11 | for (const section of this.sections.values()) { 12 | section.setEmptyVisibility(visibility); 13 | } 14 | } 15 | 16 | edit() { 17 | for (const section of this.sections.values()) { 18 | section.edit(); 19 | } 20 | } 21 | 22 | save() { 23 | for (const section of this.sections.values()) { 24 | section.save(); 25 | } 26 | } 27 | 28 | updateView() { 29 | for (const section of Array.from(this.sections.values())) { 30 | section.updateView(); 31 | } 32 | } 33 | 34 | importFromOpen5e(json) { 35 | for (const section of this.sections.values()) { 36 | section.importFromOpen5e(json); 37 | } 38 | } 39 | 40 | importFromJson(json) { 41 | for (const section of this.sections.values()) { 42 | section.importFromJson(json[section.modelPropertyName]); 43 | } 44 | } 45 | 46 | exportToJson() { 47 | const entries = Array.from(this.sections.entries()); 48 | const transformedEntries = entries.map(([key, section]) => [key, section.exportToJson()]); 49 | return Object.fromEntries(transformedEntries); 50 | } 51 | 52 | exportToHtml() { 53 | const fragment = document.createDocumentFragment(); 54 | for (const section of this.sections.values()) { 55 | if (! section.empty) { 56 | fragment.appendChild(section.exportToHtml()); 57 | } 58 | } 59 | 60 | return fragment; 61 | } 62 | 63 | exportToMarkdown() { 64 | const sections = Array.from(this.sections.values()); 65 | return sections 66 | .filter(section => ! section.empty) 67 | .map(section => section.exportToMarkdown()) 68 | .join('\n'); 69 | } 70 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/custom-autonomous-element.js: -------------------------------------------------------------------------------- 1 | import HtmlTemplates from '../../helpers/html-templates.js'; 2 | 3 | export default class CustomAutonomousElement extends HTMLElement { 4 | static get elementName() { 5 | throw new Error( 6 | `The class '${this.name}' must implement the elementName() getter.`); 7 | } 8 | 9 | static get templatePaths() { 10 | return new Map(); 11 | } 12 | 13 | static async define() { 14 | for (const [name, path] of this.templatePaths) { 15 | if (! HtmlTemplates.hasTemplate(name)) { 16 | await HtmlTemplates.addTemplate(name, path); 17 | } 18 | } 19 | 20 | customElements.define(this.elementName, this); 21 | } 22 | 23 | constructor(templatePaths) { 24 | super(); 25 | 26 | this.isInitialized = false; 27 | this.attachShadow({mode: 'open'}); 28 | 29 | for (const name of templatePaths.keys()) { 30 | const template = HtmlTemplates.getTemplate(name); 31 | const fragment = document.createRange().createContextualFragment(template); 32 | this.shadowRoot.appendChild(fragment.cloneNode(true)); 33 | } 34 | } 35 | 36 | connectedCallback() { 37 | return; 38 | } 39 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/dialogs/custom-dialog.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | import isRunningInJsdom from '../../../helpers/is-running-in-jsdom.js'; 3 | 4 | export default class CustomDialog extends CustomAutonomousElement { 5 | static get elementName() { return 'custom-dialog'; } 6 | static get templatePaths() { 7 | return super.templatePaths.set( 8 | 'custom-dialog', 9 | 'src/html/elements/autonomous/dialogs/custom-dialog.html'); 10 | } 11 | 12 | constructor(templatePaths, parent = null) { 13 | super(templatePaths, parent); 14 | 15 | this.dialog = this.shadowRoot.getElementById('dialog'); 16 | this.closeButton = this.shadowRoot.getElementById('close-button'); 17 | } 18 | 19 | connectedCallback() { 20 | super.connectedCallback(); 21 | 22 | this.closeButton.addEventListener('click', this.onClickCloseButton.bind(this)); 23 | } 24 | 25 | onClickCloseButton() { 26 | this.closeModal(); 27 | } 28 | 29 | set open(isOpen) { 30 | if (isOpen) { 31 | this.dialog.setAttribute('open', ''); 32 | } else { 33 | this.dialog.removeAttribute('open'); 34 | } 35 | } 36 | 37 | get open() { 38 | return this.dialog.getAttribute('open'); 39 | } 40 | 41 | // JSDOM doesn't support showModal() or close() methods on dialogs, so we have to manually set the "open" attribute instead. 42 | 43 | showModal() { 44 | if (isRunningInJsdom) { 45 | this.open = true; 46 | } else { 47 | this.dialog.showModal(); 48 | } 49 | } 50 | 51 | closeModal() { 52 | if (isRunningInJsdom) { 53 | this.open = false; 54 | } else { 55 | this.dialog.close(); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/dialogs/export-dialog.js: -------------------------------------------------------------------------------- 1 | import OptionDialog from './option-dialog.js'; 2 | 3 | import { startFileDownload, ClipboardWrapper } from '../../../helpers/export-helpers.js'; 4 | 5 | export default class ExportDialog extends OptionDialog { 6 | static get elementName() { return 'export-dialog'; } 7 | static get templatePaths() { 8 | return super.templatePaths.set( 9 | 'export-dialog', 10 | 'src/html/elements/autonomous/dialogs/export-dialog.html'); 11 | } 12 | 13 | constructor() { 14 | super(ExportDialog.templatePaths); 15 | 16 | this.copyToClipboardButton = this.shadowRoot.getElementById('copy-to-clipboard-button'); 17 | this.downloadAsFileButton = this.shadowRoot.getElementById('download-as-file-button'); 18 | 19 | this.clipboard = null; 20 | 21 | this.exportContent = null; 22 | this.exportContentType = null; 23 | this.exportFileName = null; 24 | } 25 | 26 | connectedCallback() { 27 | if (this.isConnected && ! this.isInitialized) { 28 | super.connectedCallback(); 29 | 30 | this.downloadAsFileButton.addEventListener('click', this.onClickDownloadAsFileButton.bind(this)); 31 | 32 | this.isInitialized = true; 33 | } 34 | } 35 | 36 | onClickDownloadAsFileButton() { 37 | startFileDownload(this.exportContent, this.exportContentType, this.exportFileName); 38 | 39 | this.setStatus('File download initiated.', 'success'); 40 | } 41 | 42 | onClickCloseButton() { 43 | super.onClickCloseButton(); 44 | 45 | this.clipboard.destroy(); 46 | } 47 | 48 | launch(content, contentType, fileName) { 49 | this.exportContent = content; 50 | this.exportContentType = contentType; 51 | this.exportFileName = fileName; 52 | 53 | this.setStatus('Choose one of the following options:'); 54 | 55 | this.clipboard = new ClipboardWrapper( 56 | content, 57 | this.dialog, 58 | this.copyToClipboardButton, 59 | ); 60 | 61 | this.clipboard.setSuccessCallback(() => { 62 | this.setStatus('Copied to clipboard.', 'success'); 63 | }); 64 | 65 | this.clipboard.setErrorCallback(() => { 66 | this.setStatus('Press Ctrl+C to copy to clipboard.', 'error'); 67 | }); 68 | 69 | this.showModal(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/dialogs/import-dialog.js: -------------------------------------------------------------------------------- 1 | import OptionDialog from './option-dialog.js'; 2 | 3 | export default class ImportDialog extends OptionDialog { 4 | static get elementName() { return 'import-dialog'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'import-dialog', 8 | 'src/html/elements/autonomous/dialogs/import-dialog.html'); 9 | } 10 | 11 | constructor(templatePaths) { 12 | super(templatePaths); 13 | 14 | this.importCallback = null; 15 | } 16 | 17 | connectedCallback() { 18 | super.connectedCallback(); 19 | } 20 | 21 | launch(importCallback) { 22 | this.importCallback = importCallback; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/dialogs/import-srd-dialog.js: -------------------------------------------------------------------------------- 1 | import ImportDialog from './import-dialog.js'; 2 | 3 | import SrdCreatureList from '../../../data/srd-creature-list.js'; 4 | import { fetchFromFile } from '../../../helpers/file-helpers.js'; 5 | 6 | export default class ImportSrdDialog extends ImportDialog { 7 | static get elementName() { return 'import-srd-dialog'; } 8 | static get templatePaths() { 9 | return super.templatePaths.set( 10 | 'import-srd-dialog', 11 | 'src/html/elements/autonomous/dialogs/import-srd-dialog.html'); 12 | } 13 | 14 | constructor() { 15 | super(ImportSrdDialog.templatePaths); 16 | 17 | this.creatureSelect = this.shadowRoot.getElementById('creature-select'); 18 | this.importButton = this.shadowRoot.getElementById('import-button'); 19 | } 20 | 21 | connectedCallback() { 22 | super.connectedCallback(); 23 | 24 | this.importButton.addEventListener('click', this.onClickImportButton.bind(this)); 25 | 26 | this.isInitialized = true; 27 | } 28 | 29 | async onClickImportButton() { 30 | const creatureSlug = this.creatureSelect.value; 31 | const path = `examples/5e-srd/${creatureSlug}.json`; 32 | const text = await fetchFromFile(path); 33 | const json = JSON.parse(text); 34 | 35 | this.importCallback(json); 36 | this.closeModal(); 37 | } 38 | 39 | launch(importCallback) { 40 | super.launch(importCallback); 41 | 42 | if (this.creatureSelect.options.length === 0) { 43 | this.creatureSelect.populate( 44 | SrdCreatureList.map(creature => new Option(creature.name, creature.slug))); 45 | } 46 | 47 | this.setStatus('Choose a creature:'); 48 | 49 | this.showModal(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/dialogs/option-dialog.js: -------------------------------------------------------------------------------- 1 | import CustomDialog from './custom-dialog.js'; 2 | 3 | const labelSuccessClass = 'option-dialog__status-label_success'; 4 | const labelErrorClass = 'option-dialog__status-label_error'; 5 | 6 | export default class OptionDialog extends CustomDialog { 7 | static get elementName() { return 'option-dialog'; } 8 | static get templatePaths() { 9 | return super.templatePaths.set( 10 | 'option-dialog', 11 | 'src/html/elements/autonomous/dialogs/option-dialog.html'); 12 | } 13 | 14 | constructor(templatePaths, parent = null) { 15 | super(templatePaths, parent); 16 | 17 | this.statusLabel = this.shadowRoot.getElementById('status-label'); 18 | this.cancelButton = this.shadowRoot.getElementById('cancel-button'); 19 | } 20 | 21 | connectedCallback() { 22 | super.connectedCallback(); 23 | 24 | this.cancelButton.addEventListener('click', this.onClickCloseButton.bind(this)); 25 | } 26 | 27 | get statusText() { 28 | return this.statusLabel.textContent; 29 | } 30 | 31 | get statusType() { 32 | if (this.statusLabel.classList.contains(labelSuccessClass)) { 33 | return 'success'; 34 | } 35 | else if (this.statusLabel.classList.contains(labelErrorClass)) { 36 | return 'error'; 37 | } 38 | 39 | return null; 40 | } 41 | 42 | setStatus(text, type = null) { 43 | this.statusLabel.textContent = text; 44 | 45 | this.statusLabel.classList.remove(labelSuccessClass); 46 | this.statusLabel.classList.remove(labelErrorClass); 47 | 48 | switch(type) { 49 | case 'success': 50 | this.statusLabel.classList.add(labelSuccessClass); 51 | break; 52 | case 'error': 53 | this.statusLabel.classList.add(labelErrorClass); 54 | break; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/dialogs/reset-dialog.js: -------------------------------------------------------------------------------- 1 | import OptionDialog from './option-dialog.js'; 2 | 3 | export default class ResetDialog extends OptionDialog { 4 | static get elementName() { return 'reset-dialog'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'reset-dialog', 8 | 'src/html/elements/autonomous/dialogs/reset-dialog.html'); 9 | } 10 | 11 | constructor() { 12 | super(ResetDialog.templatePaths); 13 | 14 | this.resetButton = this.shadowRoot.getElementById('reset-button'); 15 | 16 | this.resetCallback = null; 17 | } 18 | 19 | connectedCallback() { 20 | if (this.isConnected && ! this.isInitialized) { 21 | super.connectedCallback(); 22 | 23 | this.resetButton.addEventListener('click', this.onClickResetButton.bind(this)); 24 | 25 | this.isInitialized = true; 26 | } 27 | } 28 | 29 | onClickResetButton() { 30 | this.resetCallback(); 31 | this.closeModal(); 32 | } 33 | 34 | launch(resetCallback) { 35 | this.resetCallback = resetCallback; 36 | this.showModal(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/error-messages.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | import { focusAndSelectElement } from '../../helpers/element-helpers.js'; 3 | 4 | export default class ErrorMessages extends CustomAutonomousElement { 5 | static get elementName() { return 'error-messages'; } 6 | static get templatePaths() { 7 | return super.templatePaths.set( 8 | 'error-messages', 9 | 'src/html/elements/autonomous/error-messages.html'); 10 | } 11 | 12 | constructor() { 13 | super(ErrorMessages.templatePaths); 14 | 15 | this.container = this.shadowRoot.getElementById('error-messages'); 16 | this.list = this.shadowRoot.getElementById('error-messages-list'); 17 | 18 | this.errors = []; 19 | } 20 | 21 | add(fieldElement, message) { 22 | let error = { 23 | fieldElement: fieldElement, 24 | message: message 25 | }; 26 | 27 | this.errors.push(error); 28 | fieldElement.classList.add('section__error-highlight'); 29 | this.container.classList.remove('error-messages_hidden'); 30 | 31 | let messageElement = ErrorMessages.createErrorMessageElement(message); 32 | this.list.appendChild(messageElement); 33 | } 34 | 35 | clear() { 36 | this.container.classList.add('error-messages_hidden'); 37 | 38 | while (this.errors.length > 0) { 39 | let error = this.errors.pop(); 40 | error.fieldElement.classList.remove('section__error-highlight'); 41 | } 42 | 43 | let list = this.list; 44 | 45 | while (list.hasChildNodes()) { 46 | list.removeChild(list.lastChild); 47 | } 48 | } 49 | 50 | focusOnFirstErrorField() { 51 | focusAndSelectElement(this.errors[0].fieldElement); 52 | } 53 | 54 | get any() { 55 | return this.errors.length > 0; 56 | } 57 | 58 | static createErrorMessageElement(message) { 59 | let listItemElement = document.createElement('li'); 60 | 61 | let textNode = document.createTextNode(message); 62 | listItemElement.appendChild(textNode); 63 | 64 | return listItemElement; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/getting-started-help-box.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | const hiddenClass = 'getting-started-help-box_hidden'; 4 | 5 | export default class GettingStartedHelpBox extends CustomAutonomousElement { 6 | static get elementName() { return 'getting-started-help-box'; } 7 | static get templatePaths() { 8 | return super.templatePaths.set( 9 | 'getting-started-help-box', 10 | 'src/html/elements/autonomous/getting-started-help-box.html'); 11 | } 12 | 13 | constructor() { 14 | super(GettingStartedHelpBox.templatePaths); 15 | 16 | this.container = this.shadowRoot.getElementById('container'); 17 | this.closeButton = this.shadowRoot.getElementById('close-button'); 18 | } 19 | 20 | connectedCallback() { 21 | if (this.isConnected && ! this.isInitialized) { 22 | super.connectedCallback(); 23 | 24 | this.closeButton.addEventListener('click', this.onClickCloseButton.bind(this)); 25 | 26 | this.isInitialized = true; 27 | } 28 | } 29 | 30 | onClickCloseButton() { 31 | const event = new CustomEvent('toggleGettingStarted', { 32 | bubbles: true, 33 | composed: true 34 | }); 35 | this.dispatchEvent(event); 36 | } 37 | 38 | set visible(isVisible) { 39 | isVisible ? this.container.classList.remove(hiddenClass) : this.container.classList.add(hiddenClass); 40 | } 41 | 42 | get visible() { 43 | return ! this.container.classList.contains(hiddenClass); 44 | } 45 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/display-block-list.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class DisplayBlockList extends CustomAutonomousElement { 4 | static get elementName() { return 'display-block-list'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'display-block-list', 8 | 'src/html/elements/autonomous/lists/display-block-list.html'); 9 | } 10 | 11 | constructor(templatePaths) { 12 | super(templatePaths ? templatePaths : DisplayBlockList.templatePaths); 13 | } 14 | 15 | get blockElementTag() { 16 | return 'display-block'; 17 | } 18 | 19 | get blocks() { 20 | return Array.from(this.children); 21 | } 22 | 23 | clear() { 24 | for (const block of this.blocks) { 25 | this.removeChild(block); 26 | } 27 | } 28 | 29 | addBlock(name, text) { 30 | const block = document.createElement(this.blockElementTag); 31 | 32 | block.name = name; 33 | block.text = text; 34 | 35 | this.appendChild(block); 36 | } 37 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/display-block.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class DisplayBlock extends CustomAutonomousElement { 4 | static get elementName() { return 'display-block'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'display-block', 8 | 'src/html/elements/autonomous/lists/display-block.html'); 9 | } 10 | 11 | constructor(templatePaths) { 12 | super(templatePaths ? templatePaths : DisplayBlock.templatePaths); 13 | 14 | this.nameElement = this.shadowRoot.getElementById('display-block-name'); 15 | this.textElement = this.shadowRoot.getElementById('display-block-text'); 16 | } 17 | 18 | get name() { 19 | return this.nameElement.textContent; 20 | } 21 | 22 | set name(name) { 23 | this.nameElement.textContent = name; 24 | } 25 | 26 | get text() { 27 | return this.textElement.innerHTMLSanitized; 28 | } 29 | 30 | set text(text) { 31 | this.textElement.innerHTMLSanitized = text; 32 | } 33 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/drag-and-drop-list.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class DragAndDropList extends CustomAutonomousElement { 4 | constructor(templatePaths, parent) { 5 | super(templatePaths, parent); 6 | 7 | this.draggedItem = null; 8 | } 9 | 10 | insertDraggedItemBefore(element) { 11 | if (this.draggedItem !== null) { 12 | this.insertBefore(this.draggedItem, element); 13 | this.draggedItem = null; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/editable-block-list.js: -------------------------------------------------------------------------------- 1 | import DragAndDropList from './drag-and-drop-list.js'; 2 | 3 | import { focusAndSelectElement } from '../../../helpers/element-helpers.js'; 4 | 5 | export default class EditableBlockList extends DragAndDropList { 6 | static get elementName() { return 'editable-block-list'; } 7 | static get templatePaths() { 8 | return super.templatePaths.set( 9 | 'editable-block-list', 10 | 'src/html/elements/autonomous/lists/editable-block-list.html'); 11 | } 12 | 13 | constructor(templatePaths) { 14 | super(templatePaths ? templatePaths : EditableBlockList.templatePaths); 15 | 16 | this.singleName = null; 17 | this.isLegendaryActionList = false; 18 | } 19 | 20 | get blockElementTag() { 21 | return 'editable-block'; 22 | } 23 | 24 | get blocks() { 25 | return Array.from(this.children); 26 | } 27 | 28 | clear() { 29 | for (const block of this.blocks) { 30 | block.remove(); 31 | } 32 | } 33 | 34 | addBlock(name = '', text = '') { 35 | const block = document.createElement(this.blockElementTag); 36 | 37 | block.list = this; 38 | block.name = name; 39 | block.text = text; 40 | 41 | block.nameInput.setAttribute('pretty-name', `${this.singleName} Name`); 42 | block.textArea.setAttribute('pretty-name', `${this.singleName} Text`); 43 | 44 | this.appendChild(block); 45 | 46 | focusAndSelectElement(block.nameInput); 47 | 48 | return block; 49 | } 50 | 51 | validate(errorMessages) { 52 | for (const block of this.blocks) { 53 | block.validate(errorMessages); 54 | } 55 | } 56 | 57 | parse() { 58 | for (const block of this.blocks) { 59 | block.parse(); 60 | } 61 | } 62 | 63 | toModel() { 64 | return this.blocks.map(block => block.toModel()); 65 | } 66 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/legendary-action-display-block-list.js: -------------------------------------------------------------------------------- 1 | import DisplayBlockList from './display-block-list.js'; 2 | 3 | export default class LegendaryActionDisplayBlockList extends DisplayBlockList { 4 | static get elementName() { return 'legendary-action-display-block-list'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'legendary-action-display-block-list', 8 | 'src/html/elements/autonomous/lists/legendary-action-display-block-list.html'); 9 | } 10 | 11 | constructor() { 12 | super(LegendaryActionDisplayBlockList.templatePaths); 13 | } 14 | 15 | get blockElementTag() { 16 | return 'legendary-action-display-block'; 17 | } 18 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/legendary-action-display-block.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | import DisplayBlock from './display-block.js'; 3 | 4 | export default class LegendaryActionDisplayBlock extends DisplayBlock { 5 | static get elementName() { return 'legendary-action-display-block'; } 6 | static get templatePaths() { 7 | // Override the HTML template for DisplayBlock in order to use CSS specific to LegendaryActionDisplayBlocks. 8 | return CustomAutonomousElement.templatePaths.set( 9 | 'legendary-action-display-block', 10 | 'src/html/elements/autonomous/lists/legendary-action-display-block.html'); 11 | } 12 | 13 | constructor() { 14 | super(LegendaryActionDisplayBlock.templatePaths); 15 | } 16 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/legendary-action-editable-block-list.js: -------------------------------------------------------------------------------- 1 | import EditableBlockList from './editable-block-list.js'; 2 | 3 | export default class LegendaryActionEditableBlockList extends EditableBlockList { 4 | static get elementName() { return 'legendary-action-editable-block-list'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'legendary-action-editable-block-list', 8 | 'src/html/elements/autonomous/lists/legendary-action-editable-block-list.html'); 9 | } 10 | 11 | constructor() { 12 | super(LegendaryActionEditableBlockList.templatePaths); 13 | } 14 | 15 | get blockElementTag() { 16 | return 'legendary-action-editable-block'; 17 | } 18 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/legendary-action-editable-block.js: -------------------------------------------------------------------------------- 1 | import DragAndDropListItem from './drag-and-drop-list-item.js'; 2 | import EditableBlock from './editable-block.js'; 3 | 4 | import LegendaryBlockModel from '../../../models/lists/block/legendary-block-model.js'; 5 | 6 | export default class LegendaryActionEditableBlock extends EditableBlock { 7 | static get elementName() { return 'legendary-action-editable-block'; } 8 | static get templatePaths() { 9 | // Override the HTML template for EditableBlock in order to use CSS specific to LegendaryActionEditableBlocks. 10 | return DragAndDropListItem.templatePaths.set( 11 | 'legendary-action-editable-block', 12 | 'src/html/elements/autonomous/lists/legendary-action-editable-block.html'); 13 | } 14 | 15 | constructor() { 16 | super(LegendaryActionEditableBlock.templatePaths); 17 | } 18 | 19 | toModel() { 20 | return new LegendaryBlockModel( 21 | this.name, 22 | this.text, 23 | this.markdownText, 24 | this.htmlText); 25 | } 26 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/lists/property-list-item.js: -------------------------------------------------------------------------------- 1 | import DragAndDropListItem from './drag-and-drop-list-item.js'; 2 | 3 | export default class PropertyListItem extends DragAndDropListItem { 4 | static get elementName() { return 'property-list-item'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'property-list-item', 8 | 'src/html/elements/autonomous/lists/property-list-item.html'); 9 | } 10 | 11 | constructor(parent, text) { 12 | super(PropertyListItem.templatePaths, parent); 13 | 14 | this.label = this.shadowRoot.getElementById('property-list-item-label'); 15 | this.removeButton = this.shadowRoot.getElementById('property-list-item-remove-button'); 16 | 17 | this.dragImage = this.label; 18 | 19 | this.text = text; 20 | } 21 | 22 | connectedCallback() { 23 | if (this.isConnected && ! this.isInitialized) { 24 | super.connectedCallback(); 25 | 26 | this.removeButton.addEventListener('click', this.onClickRemoveButton.bind(this)); 27 | 28 | this.isInitialized = true; 29 | } 30 | } 31 | 32 | onClickRemoveButton() { 33 | this.remove(); 34 | this.list.dispatchPropertyListChangedEvent(); 35 | } 36 | 37 | onDropItem(event) { 38 | super.onDropItem(event); 39 | 40 | this.list.dispatchPropertyListChangedEvent(); 41 | } 42 | 43 | get text() { 44 | return this.label.textContent; 45 | } 46 | 47 | set text(text) { 48 | this.label.textContent = text; 49 | } 50 | 51 | remove() { 52 | this.list.removeChild(this); 53 | this.list.dataList.setOptionEnabled(this.text, true); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/loading-screen.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | const hiddenClass = 'loading-screen_hidden'; 4 | 5 | export default class LoadingScreen extends CustomAutonomousElement { 6 | static get elementName() { return 'loading-screen'; } 7 | static get templatePaths() { 8 | return super.templatePaths.set( 9 | 'loading-screen', 10 | 'src/html/elements/autonomous/loading-screen.html'); 11 | } 12 | 13 | constructor() { 14 | super(LoadingScreen.templatePaths); 15 | 16 | this.container = this.shadowRoot.getElementById('container'); 17 | this.statusElement = this.shadowRoot.getElementById('status'); 18 | } 19 | 20 | set visible(isVisible) { 21 | if(isVisible) { 22 | this.container.classList.remove(hiddenClass); 23 | } else { 24 | this.container.classList.add(hiddenClass); 25 | } 26 | } 27 | 28 | get visible() { 29 | return ! this.container.classList.contains(hiddenClass); 30 | } 31 | 32 | set status(text) { 33 | this.statusElement.textContent = text; 34 | } 35 | 36 | get status() { 37 | return this.statusElement.textContent; 38 | } 39 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/menus/drop-down-menu.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class DropDownMenu extends CustomAutonomousElement { 4 | static get elementName() { return 'drop-down-menu'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'drop-down-menu', 8 | 'src/html/elements/autonomous/menus/drop-down-menu.html'); 9 | } 10 | 11 | constructor() { 12 | super(DropDownMenu.templatePaths); 13 | } 14 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/menus/expression-menu.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class ExpressionMenu extends CustomAutonomousElement { 4 | static get elementName() { return 'expression-menu'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'expression-menu', 8 | 'src/html/elements/autonomous/menus/expression-menu.html'); 9 | } 10 | 11 | constructor() { 12 | super(ExpressionMenu.templatePaths); 13 | 14 | this.items = this.shadowRoot.getElementById('expression-menu-items'); 15 | } 16 | 17 | connectedCallback() { 18 | if (this.isConnected && ! this.isInitialized) { 19 | super.connectedCallback(); 20 | 21 | this.items.addEventListener('click', this.onClickItems.bind(this)); 22 | 23 | this.initialized = true; 24 | } 25 | } 26 | 27 | async onClickItems() { 28 | // When an item is clicked, apply the hidden style, wait briefly, then remove the hidden style. 29 | // The delay is necessary to ensure that the items are actually hidden. 30 | this.items.style.visibility = 'hidden'; 31 | await new Promise(resolve => setTimeout(resolve, 100)); 32 | this.items.style.visibility = ''; 33 | } 34 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/property-block.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | export default class PropertyBlock extends CustomAutonomousElement { 4 | static get elementName() { return 'property-block'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'property-block', 8 | 'src/html/elements/autonomous/property-block.html'); 9 | } 10 | 11 | constructor() { 12 | super(PropertyBlock.templatePaths); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/property-line.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | export default class PropertyLine extends CustomAutonomousElement { 4 | static get elementName() { return 'property-line'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'property-line', 8 | 'src/html/elements/autonomous/property-line.html'); 9 | } 10 | 11 | constructor() { 12 | super(PropertyLine.templatePaths); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/section-divider.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | export default class SectionDivider extends CustomAutonomousElement { 4 | static get elementName() { return 'section-divider'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'section-divider', 8 | 'src/html/elements/autonomous/section-divider.html'); 9 | } 10 | 11 | static get hiddenClassName() { return 'section-divider_hidden'; } 12 | 13 | constructor() { 14 | super(SectionDivider.templatePaths); 15 | this.divider = this.shadowRoot.querySelector('.section-divider'); 16 | this.hidden = true; 17 | } 18 | 19 | get hidden() { 20 | return this.divider.classList.contains(SectionDivider.hiddenClassName); 21 | } 22 | 23 | set hidden(isHidden) { 24 | if (isHidden) { 25 | this.divider.classList.add(SectionDivider.hiddenClassName); 26 | } else { 27 | this.divider.classList.remove(SectionDivider.hiddenClassName); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/actions-section.js: -------------------------------------------------------------------------------- 1 | import { BlockListSection, BlockListShowElements, BlockListEditElements } from './block-list-section.js'; 2 | 3 | export default class ActionsSection extends BlockListSection { 4 | static get elementName() { return 'actions-section'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'actions-section', 8 | 'src/html/elements/autonomous/sections/actions-section.html'); 9 | } 10 | 11 | constructor() { 12 | super(ActionsSection.templatePaths, 13 | 'actions', 14 | ActionsSectionShowElements, 15 | ActionsSectionEditElements); 16 | 17 | this.empty = true; 18 | } 19 | 20 | connectedCallback() { 21 | if (this.isConnected && ! this.isInitialized) { 22 | super.connectedCallback(); 23 | 24 | this.editElements.generateAttackButton.addEventListener('click', this.onClickGenerateAttackButton.bind(this)); 25 | 26 | this.addEventListener('generateAttack', this.onGenerateAttack.bind(this)); 27 | 28 | this.isInitialized = true; 29 | } 30 | } 31 | 32 | onClickGenerateAttackButton() { 33 | this.editElements.generateAttackDialog.launch(); 34 | } 35 | 36 | onGenerateAttack(event) { 37 | this.addBlock(event.detail.name, event.detail.text); 38 | this.reparse(); 39 | } 40 | } 41 | 42 | class ActionsSectionShowElements extends BlockListShowElements { 43 | constructor(shadowRoot) { 44 | super(shadowRoot); 45 | } 46 | } 47 | 48 | class ActionsSectionEditElements extends BlockListEditElements { 49 | constructor(shadowRoot) { 50 | super(shadowRoot); 51 | 52 | this.generateAttackButton = shadowRoot.getElementById('generate-attack-button'); 53 | this.generateAttackDialog = shadowRoot.getElementById('generate-attack-dialog'); 54 | } 55 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/condition-immunities-section.js: -------------------------------------------------------------------------------- 1 | import { PropertyListSection } from './property-list-section.js'; 2 | import Conditions from '../../../data/conditions.js'; 3 | 4 | export default class ConditionImmunitiesSection extends PropertyListSection { 5 | static get elementName() { return 'condition-immunities-section'; } 6 | static get templatePaths() { 7 | return super.templatePaths.set( 8 | 'condition-immunities-section', 9 | 'src/html/elements/autonomous/sections/condition-immunities-section.html'); 10 | } 11 | 12 | constructor() { 13 | super(ConditionImmunitiesSection.templatePaths, 14 | 'conditionImmunities'); 15 | 16 | this.empty = true; 17 | } 18 | 19 | connectedCallback() { 20 | if (this.isConnected && ! this.isInitialized) { 21 | super.connectedCallback(); 22 | 23 | this.editElements.propertyList.dataListOptions = Conditions.map(condition => ({ text: condition }) ); 24 | 25 | this.isInitialized = true; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/damage-immunities-section.js: -------------------------------------------------------------------------------- 1 | import { PropertyListSection } from './property-list-section.js'; 2 | import DamageTypesForPropertyLists from '../../../data/damage-types-for-property-lists.js'; 3 | 4 | export default class DamageImmunitiesSection extends PropertyListSection { 5 | static get elementName() { return 'damage-immunities-section'; } 6 | static get templatePaths() { 7 | return super.templatePaths.set( 8 | 'damage-immunities-section', 9 | 'src/html/elements/autonomous/sections/damage-immunities-section.html'); 10 | } 11 | 12 | constructor() { 13 | super(DamageImmunitiesSection.templatePaths, 14 | 'damageImmunities'); 15 | 16 | this.empty = true; 17 | } 18 | 19 | connectedCallback() { 20 | if (this.isConnected && ! this.isInitialized) { 21 | super.connectedCallback(); 22 | 23 | this.editElements.propertyList.dataListOptions = DamageTypesForPropertyLists.map(damageType => ({ text: damageType }) ); 24 | 25 | this.isInitialized = true; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/damage-resistances-section.js: -------------------------------------------------------------------------------- 1 | import { PropertyListSection } from './property-list-section.js'; 2 | import DamageTypesForPropertyLists from '../../../data/damage-types-for-property-lists.js'; 3 | 4 | export default class DamageResistancesSection extends PropertyListSection { 5 | static get elementName() { return 'damage-resistances-section'; } 6 | static get templatePaths() { 7 | return super.templatePaths.set( 8 | 'damage-resistances-section', 9 | 'src/html/elements/autonomous/sections/damage-resistances-section.html'); 10 | } 11 | 12 | constructor() { 13 | super(DamageResistancesSection.templatePaths, 14 | 'damageResistances'); 15 | 16 | this.empty = true; 17 | } 18 | 19 | connectedCallback() { 20 | if (this.isConnected && ! this.isInitialized) { 21 | super.connectedCallback(); 22 | 23 | this.editElements.propertyList.dataListOptions = DamageTypesForPropertyLists.map(damageType => ({ text: damageType }) ); 24 | 25 | this.isInitialized = true; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/damage-vulnerabilities-section.js: -------------------------------------------------------------------------------- 1 | import { PropertyListSection } from './property-list-section.js'; 2 | import DamageTypesForPropertyLists from '../../../data/damage-types-for-property-lists.js'; 3 | 4 | export default class DamageVulnerabilitiesSection extends PropertyListSection { 5 | static get elementName() { return 'damage-vulnerabilities-section'; } 6 | static get templatePaths() { 7 | return super.templatePaths.set( 8 | 'damage-vulnerabilities-section', 9 | 'src/html/elements/autonomous/sections/damage-vulnerabilities-section.html'); 10 | } 11 | 12 | constructor() { 13 | super(DamageVulnerabilitiesSection.templatePaths, 14 | 'damageVulnerabilities'); 15 | 16 | this.empty = true; 17 | } 18 | 19 | connectedCallback() { 20 | if (this.isConnected && ! this.isInitialized) { 21 | super.connectedCallback(); 22 | 23 | this.editElements.propertyList.dataListOptions = DamageTypesForPropertyLists.map(damageType => ({ text: damageType }) ); 24 | 25 | this.isInitialized = true; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/languages-section.js: -------------------------------------------------------------------------------- 1 | import { PropertyListSection, PropertyListShowElements, PropertyListEditElements } from './property-list-section.js'; 2 | import CurrentContext from '../../../models/current-context.js'; 3 | import Languages from '../../../data/languages.js'; 4 | 5 | export default class LanguagesSection extends PropertyListSection { 6 | static get elementName() { return 'languages-section'; } 7 | static get templatePaths() { 8 | return super.templatePaths.set( 9 | 'languages-section', 10 | 'src/html/elements/autonomous/sections/languages-section.html'); 11 | } 12 | 13 | constructor() { 14 | super(LanguagesSection.templatePaths, 15 | 'languages', 16 | LanguagesSectionShowElements, 17 | LanguagesSectionEditElements); 18 | } 19 | 20 | connectedCallback() { 21 | if (this.isConnected && ! this.isInitialized) { 22 | super.connectedCallback(); 23 | 24 | this.editElements.propertyList.dataListOptions = Languages.map(language => ({ text: language })); 25 | 26 | this.isInitialized = true; 27 | } 28 | } 29 | 30 | updateModel() { 31 | super.updateModel(); 32 | 33 | CurrentContext.creature.languages.telepathy = this.editElements.telepathy.valueAsInt; 34 | } 35 | 36 | updateEditModeView() { 37 | super.updateEditModeView(); 38 | 39 | this.editElements.telepathy.value = CurrentContext.creature.languages.telepathy; 40 | } 41 | } 42 | 43 | class LanguagesSectionShowElements extends PropertyListShowElements { 44 | constructor(shadowRoot) { 45 | super(shadowRoot); 46 | } 47 | } 48 | 49 | class LanguagesSectionEditElements extends PropertyListEditElements { 50 | constructor(shadowRoot) { 51 | super(shadowRoot); 52 | 53 | this.telepathy = shadowRoot.getElementById('telepathy-input'); 54 | } 55 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/property-line-section.js: -------------------------------------------------------------------------------- 1 | import { Section, ShowElements, EditElements } from './section.js'; 2 | 3 | export class PropertyLineSection extends Section { 4 | constructor(templatePaths, modelPropertyName, showElements, editElements) { 5 | super(templatePaths, modelPropertyName, showElements, editElements); 6 | } 7 | } 8 | 9 | export class PropertyLineShowElements extends ShowElements { 10 | constructor(shadowRoot) { 11 | super(shadowRoot); 12 | this.heading = shadowRoot.getElementById('show-section-heading'); 13 | this.text = shadowRoot.getElementById('show-section-text'); 14 | } 15 | } 16 | 17 | export class PropertyLineEditElements extends EditElements { 18 | constructor(shadowRoot) { 19 | super(shadowRoot); 20 | } 21 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/reactions-section.js: -------------------------------------------------------------------------------- 1 | import { BlockListSection } from './block-list-section.js'; 2 | 3 | export default class ReactionsSection extends BlockListSection { 4 | static get elementName() { return 'reactions-section'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'reactions-section', 8 | 'src/html/elements/autonomous/sections/reactions-section.html'); 9 | } 10 | 11 | constructor() { 12 | super(ReactionsSection.templatePaths, 13 | 'reactions'); 14 | 15 | this.empty = true; 16 | } 17 | 18 | connectedCallback() { 19 | if (this.isConnected && ! this.isInitialized) { 20 | super.connectedCallback(); 21 | 22 | this.isInitialized = true; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/sections/special-traits-section.js: -------------------------------------------------------------------------------- 1 | import { BlockListSection, BlockListShowElements, BlockListEditElements } from './block-list-section.js'; 2 | 3 | export default class SpecialTraitsSection extends BlockListSection { 4 | static get elementName() { return 'special-traits-section'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'special-traits-section', 8 | 'src/html/elements/autonomous/sections/special-traits-section.html'); 9 | } 10 | 11 | constructor() { 12 | super(SpecialTraitsSection.templatePaths, 13 | 'specialTraits', 14 | SpecialTraitsSectionShowElements, 15 | SpecialTraitsSectionEditElements); 16 | 17 | this.empty = true; 18 | } 19 | 20 | connectedCallback() { 21 | if (this.isConnected && ! this.isInitialized) { 22 | super.connectedCallback(); 23 | 24 | this.editElements.generateSpellcastingButton.addEventListener('click', this.onClickGenerateSpellcastingButton.bind(this)); 25 | 26 | this.addEventListener('generateSpellcasting', this.onGenerateSpellcasting.bind(this)); 27 | 28 | this.isInitialized = true; 29 | } 30 | } 31 | 32 | onClickGenerateSpellcastingButton() { 33 | this.editElements.generateSpellcastingDialog.launch(); 34 | } 35 | 36 | onGenerateSpellcasting(event) { 37 | this.addBlock(event.detail.name, event.detail.text); 38 | this.reparse(); 39 | } 40 | } 41 | 42 | class SpecialTraitsSectionShowElements extends BlockListShowElements { 43 | constructor(shadowRoot) { 44 | super(shadowRoot); 45 | } 46 | } 47 | 48 | class SpecialTraitsSectionEditElements extends BlockListEditElements { 49 | constructor(shadowRoot) { 50 | super(shadowRoot); 51 | 52 | this.generateSpellcastingButton = shadowRoot.getElementById('generate-spellcasting-button'); 53 | this.generateSpellcastingDialog = shadowRoot.getElementById('generate-spellcasting-dialog'); 54 | } 55 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/slide-toggle.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | export default class SlideToggle extends CustomAutonomousElement { 4 | static get elementName() { return 'slide-toggle'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'slide-toggle', 8 | 'src/html/elements/autonomous/slide-toggle.html'); 9 | } 10 | 11 | constructor() { 12 | super(SlideToggle.templatePaths); 13 | 14 | this.checkbox = this.shadowRoot.getElementById('slide-toggle-checkbox'); 15 | this.control = this.shadowRoot.getElementById('slide-toggle-control'); 16 | } 17 | 18 | connectedCallback() { 19 | if (this.isConnected && ! this.isInitialized) { 20 | super.connectedCallback(); 21 | 22 | this.control.dataset.uncheckedText = this.dataset.uncheckedText ? this.dataset.uncheckedText : 'Off'; 23 | this.control.dataset.checkedText = this.dataset.checkedText ? this.dataset.checkedText : 'On'; 24 | 25 | const orientationClass = this.dataset.orientation === 'vertical' ? 'slide-toggle__vertical-control' : 'slide-toggle__horizontal-control'; 26 | this.control.classList.add(orientationClass); 27 | 28 | this.checked = this.hasAttribute('checked'); 29 | 30 | this.isInitialized = true; 31 | } 32 | } 33 | 34 | get checked() { 35 | return this.checkbox.checked; 36 | } 37 | 38 | set checked(isChecked) { 39 | this.checkbox.checked = isChecked; 40 | } 41 | 42 | addEventListener(type, listener) { 43 | this.checkbox.addEventListener(type, listener); 44 | } 45 | 46 | click() { 47 | this.checkbox.click(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/spell-category-box.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | export default class SpellCategoryBox extends CustomAutonomousElement { 4 | static get elementName() { return 'spell-category-box'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'spell-category-box', 8 | 'src/html/elements/autonomous/spell-category-box.html'); 9 | } 10 | 11 | constructor(parent) { 12 | super(SpellCategoryBox.templatePaths, parent); 13 | 14 | this.container = this.shadowRoot.getElementById('container'); 15 | this.heading = this.shadowRoot.getElementById('heading'); 16 | this.propertyList = this.shadowRoot.getElementById('property-list'); 17 | 18 | this.propertyList.singleItemName = 'spell'; 19 | } 20 | 21 | connectedCallback() { 22 | if (this.isConnected && ! this.isInitialized) { 23 | super.connectedCallback(); 24 | 25 | this.isInitialized = true; 26 | } 27 | } 28 | 29 | get disabled() { 30 | return this.hasAttribute('disabled'); 31 | } 32 | 33 | set disabled(isDisabled) { 34 | const containerDisabledClass = 'spell-category__container_disabled'; 35 | 36 | if (isDisabled) { 37 | this.setAttribute('disabled', ''); 38 | this.container.classList.add(containerDisabledClass); 39 | } else { 40 | this.removeAttribute('disabled'); 41 | this.container.classList.remove(containerDisabledClass); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/tapered-rule.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from './custom-autonomous-element.js'; 2 | 3 | export default class TaperedRule extends CustomAutonomousElement { 4 | static get elementName() { return 'tapered-rule'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'tapered-rule', 8 | 'src/html/elements/autonomous/tapered-rule.html'); 9 | } 10 | 11 | constructor() { 12 | super(TaperedRule.templatePaths); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/js/elements/autonomous/tooltips/custom-text-help-tooltip.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class CustomTextHelpTooltip extends CustomAutonomousElement { 4 | static get elementName() { return 'custom-text-help-tooltip'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'custom-text-help-tooltip', 8 | 'src/html/elements/autonomous/tooltips/custom-text-help-tooltip.html'); 9 | } 10 | 11 | constructor() { 12 | super(CustomTextHelpTooltip.templatePaths); 13 | } 14 | } -------------------------------------------------------------------------------- /src/js/elements/autonomous/tooltips/help-tooltip.js: -------------------------------------------------------------------------------- 1 | import CustomAutonomousElement from '../custom-autonomous-element.js'; 2 | 3 | export default class HelpTooltip extends CustomAutonomousElement { 4 | static get elementName() { return 'help-tooltip'; } 5 | static get templatePaths() { 6 | return super.templatePaths.set( 7 | 'help-tooltip', 8 | 'src/html/elements/autonomous/tooltips/help-tooltip.html'); 9 | } 10 | 11 | constructor() { 12 | super(HelpTooltip.templatePaths); 13 | } 14 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/block-textarea.js: -------------------------------------------------------------------------------- 1 | import { escapeHtml } from '../../helpers/string-formatter.js'; 2 | import { parseAll } from '../../parsers/parser.js'; 3 | 4 | export default class BlockTextArea extends HTMLTextAreaElement { 5 | static async define() { 6 | const elementName = 'block-textarea'; 7 | customElements.define(elementName, this, { extends: 'textarea' }); 8 | } 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.htmlText = null; 14 | } 15 | 16 | get fieldName() { 17 | const prettyName = this.getAttribute('pretty-name'); 18 | return (prettyName ? prettyName : this.name); 19 | } 20 | 21 | validate(errorMessages) { 22 | if (this.required && this.value === '') { 23 | errorMessages.add(this, `${this.fieldName} cannot be blank.`); 24 | } 25 | this.parse(errorMessages); 26 | } 27 | 28 | parse(errorMessages = null) { 29 | const escapedText = escapeHtml(this.value); 30 | 31 | const parserResults = parseAll(escapedText); 32 | 33 | if (errorMessages) { 34 | if (parserResults.nameParserResults.error) { 35 | errorMessages.add(this, `${this.fieldName} has at least one invalid name expression.`); 36 | } else if (parserResults.mathParserResults.error) { 37 | errorMessages.add(this, `${this.fieldName} has at least one invalid math expression.`); 38 | } else if (parserResults.markdownParserResults.error) { 39 | errorMessages.add(this, `${this.fieldName} has invalid markdown syntax.`); 40 | } 41 | } 42 | 43 | if (! parserResults.mathParserResults.error) { 44 | this.markdownText = parserResults.mathParserResults.outputText.replace(/\n/g, ' \n> '); 45 | } 46 | 47 | this.htmlText = parserResults.text; 48 | } 49 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/dynamic-select.js: -------------------------------------------------------------------------------- 1 | export default class DynamicSelect extends HTMLSelectElement { 2 | static async define() { 3 | const elementName = 'dynamic-select'; 4 | customElements.define(elementName, this, { extends: 'select' }); 5 | } 6 | 7 | constructor() { 8 | super(); 9 | } 10 | 11 | enable() { 12 | this.removeAttribute('disabled'); 13 | } 14 | 15 | disable() { 16 | this.setAttribute('disabled', ''); 17 | } 18 | 19 | get isEnabled() { 20 | return ! this.hasAttribute('disabled'); 21 | } 22 | 23 | clear() { 24 | while (this.options.length > 0) { 25 | this.remove(0); 26 | } 27 | } 28 | 29 | populate(optionList) { 30 | for (const option of optionList) { 31 | this.add(option); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/js/elements/builtin/enable-disable-elements-checkbox.js: -------------------------------------------------------------------------------- 1 | import { focusAndSelectElement } from '../../helpers/element-helpers.js'; 2 | 3 | export default class EnableDisableElementsCheckbox extends HTMLInputElement { 4 | static async define() { 5 | const elementName = 'enable-disable-elements-checkbox'; 6 | customElements.define(elementName, this, { extends: 'input' }); 7 | } 8 | 9 | constructor() { 10 | super(); 11 | 12 | this.enabledElements = []; 13 | this.disabledElements = []; 14 | } 15 | 16 | connectedCallback() { 17 | if (this.isConnected && ! this.isInitialized) { 18 | this.addEventListener('input', this.onInputCheckbox.bind(this)); 19 | 20 | this.initialized = true; 21 | } 22 | } 23 | 24 | onInputCheckbox() { 25 | const elementsToEnable = this.checked ? this.enabledElements : this.disabledElements; 26 | const elementsToDisable = this.checked ? this.disabledElements : this.enabledElements; 27 | 28 | for (const [index, element] of elementsToEnable.entries()) { 29 | element.removeAttribute('disabled'); 30 | if (index === 0) { 31 | focusAndSelectElement(element); 32 | } 33 | } 34 | for (const element of elementsToDisable) { 35 | element.setAttribute('disabled', ''); 36 | } 37 | } 38 | 39 | enableElementsWhenChecked(...elements) { 40 | this.enabledElements = this.enabledElements.concat(elements); 41 | } 42 | 43 | disableElementsWhenChecked(...elements) { 44 | this.disabledElements = this.disabledElements.concat(elements); 45 | } 46 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/number-input.js: -------------------------------------------------------------------------------- 1 | import { convertToInteger } from '../../helpers/number-helpers.js'; 2 | 3 | export default class NumberInput extends HTMLInputElement { 4 | static async define() { 5 | const elementName = 'number-input'; 6 | customElements.define(elementName, this, { extends: 'input' }); 7 | } 8 | 9 | constructor() { 10 | super(); 11 | } 12 | 13 | connectedCallback() { 14 | if (this.isConnected && ! this.isInitialized) { 15 | this.addEventListener('input', this.onInput); 16 | 17 | this.initialized = true; 18 | } 19 | } 20 | 21 | onInput() { 22 | if (this.value) { 23 | let value = this.valueAsInt; 24 | 25 | if (this.min && value < this.minAsInt) { 26 | this.value = this.min; 27 | } else if(this.max && value > this.maxAsInt) { 28 | this.value = this.max; 29 | } else { 30 | // Used to eliminate leading zeroes from the inputted value 31 | this.value = value; 32 | } 33 | } 34 | } 35 | 36 | validate(errorMessages) { 37 | if (this.valueAsInt === null) { 38 | const prettyName = this.getAttribute('pretty-name'); 39 | const fieldName = prettyName ? prettyName : this.name; 40 | errorMessages.add(this, `${fieldName} must be a valid number.`); 41 | } 42 | } 43 | 44 | get valueAsInt() { 45 | return convertToInteger(this.value); 46 | } 47 | 48 | get minAsInt() { 49 | return convertToInteger(this.min); 50 | } 51 | 52 | get maxAsInt() { 53 | return convertToInteger(this.max); 54 | } 55 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/number-select.js: -------------------------------------------------------------------------------- 1 | import { convertToInteger } from '../../helpers/number-helpers.js'; 2 | 3 | export default class NumberSelect extends HTMLSelectElement { 4 | static async define() { 5 | const elementName = 'number-select'; 6 | customElements.define(elementName, this, { extends: 'select' }); 7 | } 8 | 9 | constructor() { 10 | super(); 11 | } 12 | 13 | get valueAsInt() { 14 | return convertToInteger(this.value); 15 | } 16 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/property-datalist.js: -------------------------------------------------------------------------------- 1 | export default class PropertyDataList extends HTMLDataListElement { 2 | static async define() { 3 | const elementName = 'property-datalist'; 4 | customElements.define(elementName, this, { extends: 'datalist' }); 5 | } 6 | 7 | constructor() { 8 | super(); 9 | } 10 | 11 | setOptionEnabled(optionText, isEnabled) { 12 | const item = this.findOption(optionText); 13 | 14 | if (item !== null) { 15 | if (isEnabled) { 16 | item.removeAttribute('disabled'); 17 | } else { 18 | item.setAttribute('disabled', ''); 19 | } 20 | } 21 | } 22 | 23 | findOption(optionText) { 24 | for (const child of this.childNodes) { 25 | if (child.tagName === 'OPTION' && child.value === optionText) { 26 | return child; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/sanitized-paragraph.js: -------------------------------------------------------------------------------- 1 | import sanitizeHTML from '../../helpers/sanitize-html.js'; 2 | 3 | export default class SanitizedParagraph extends HTMLParagraphElement { 4 | static async define() { 5 | const elementName = 'sanitized-paragraph'; 6 | customElements.define(elementName, this, { extends: 'p' }); 7 | } 8 | 9 | constructor() { 10 | super(); 11 | } 12 | 13 | set innerHTMLSanitized(html) { 14 | this.innerHTML = sanitizeHTML(html); 15 | } 16 | 17 | get innerHTMLSanitized() { 18 | return this.innerHTML; 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/elements/builtin/text-input.js: -------------------------------------------------------------------------------- 1 | import { escapeHtml } from '../../helpers/string-formatter.js'; 2 | import { parseMarkdown } from '../../parsers/parser.js'; 3 | 4 | export default class TextInput extends HTMLInputElement { 5 | static async define() { 6 | const elementName = 'text-input'; 7 | customElements.define(elementName, this, { extends: 'input' }); 8 | } 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.htmlText = ''; 14 | } 15 | 16 | get fieldName() { 17 | const prettyName = this.getAttribute('pretty-name'); 18 | return (prettyName ? prettyName : this.name); 19 | } 20 | 21 | validate(errorMessages) { 22 | if (this.required && this.value === '') { 23 | errorMessages.add(this, `${this.fieldName} cannot be blank.`); 24 | } else if ('parsed' in this.dataset) { 25 | this.parse(errorMessages); 26 | } 27 | } 28 | 29 | parse(errorMessages) { 30 | const escapedText = escapeHtml(this.value); 31 | const parserResults = parseMarkdown(escapedText); 32 | 33 | if (parserResults.error) { 34 | const message = `${this.fieldName} has invalid Markdown syntax.`; 35 | errorMessages.add(this, message); 36 | } else { 37 | this.htmlText = parserResults.outputText; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/js/helpers/array-helpers.js: -------------------------------------------------------------------------------- 1 | export function arrayStrictEqual(array1, array2) { 2 | return array1.length === array2.length && array1.every((value, index) => value === array2[index]); 3 | } -------------------------------------------------------------------------------- /src/js/helpers/element-helpers.js: -------------------------------------------------------------------------------- 1 | export function focusAndSelectElement(element) { 2 | const tagName = element.tagName; 3 | const type = element.getAttribute('type'); 4 | 5 | if (isTextOrNumberInputElement(tagName, type) || 6 | isTextAreaElement(tagName)) { 7 | element.focus(); 8 | element.select(); 9 | } else { 10 | element.focus(); 11 | } 12 | } 13 | 14 | function isTextOrNumberInputElement(tagName, type) { 15 | return tagName === 'INPUT' && (type === 'text' || type === 'number'); 16 | } 17 | 18 | function isTextAreaElement(tagName) { 19 | return tagName === 'TEXTAREA'; 20 | } 21 | 22 | export function inputValueAndTriggerEvent(element, value) { 23 | const tagName = element.tagName; 24 | const type = element.getAttribute('type'); 25 | 26 | if (tagName === 'INPUT' && type === 'checkbox') { 27 | element.checked = value; 28 | } else { 29 | element.value = value; 30 | } 31 | 32 | element.dispatchEvent(new Event('input')); 33 | } 34 | 35 | export function addOptionsToElement(element, objects, textPropertyName = 'text', valuePropertyName = 'value') { 36 | for (const obj of objects) { 37 | const option = new Option(obj[textPropertyName], obj[valuePropertyName]); 38 | element.appendChild(option); 39 | } 40 | } 41 | 42 | export function addTextOnlyOptionsToElement(element, optionTexts) { 43 | addOptionsToElement(element, optionTexts.map(text => ({ text: text }) )); 44 | } 45 | 46 | export function addValueOnlyOptionsToElement(element, optionValues) { 47 | addOptionsToElement(element, optionValues.map(value => ({ value: value }) )); 48 | } 49 | 50 | export function removeAllChildElements(element) { 51 | while (element.firstChild) { 52 | element.removeChild(element.firstChild); 53 | } 54 | } 55 | 56 | export function getCheckedRadioButton(parentElement, radioGroupName) { 57 | return parentElement.shadowRoot.querySelector(`input[name="${radioGroupName}"]:checked`); 58 | } -------------------------------------------------------------------------------- /src/js/helpers/export-helpers.js: -------------------------------------------------------------------------------- 1 | const ClipboardJS = require('clipboard'); 2 | 3 | export class ClipboardWrapper { 4 | constructor( 5 | copiedText, 6 | container, 7 | targetElement) { 8 | 9 | this.clipboard = new ClipboardJS(targetElement, { 10 | container: container, 11 | text: function() { 12 | return copiedText; 13 | } 14 | }); 15 | } 16 | 17 | setSuccessCallback(callback) { 18 | this.clipboard.on('success', callback); 19 | } 20 | 21 | setErrorCallback(callback) { 22 | this.clipboard.on('error', callback); 23 | } 24 | 25 | destroy() { 26 | this.clipboard.destroy(); 27 | } 28 | } 29 | 30 | export function startFileDownload(content, contentType, fileName) { 31 | const blob = new Blob([content], {type: contentType}); 32 | const link = document.createElement('a'); 33 | link.download = fileName; 34 | link.href = URL.createObjectURL(blob); 35 | link.click(); 36 | } 37 | 38 | export function createHtmlPropertyLine(heading, text) { 39 | const propertyLine = document.createElement('property-line'); 40 | return populatePropertyElement(propertyLine, heading, text); 41 | } 42 | 43 | export function createHtmlPropertyBlock(heading, text) { 44 | const propertyBlock = document.createElement('property-block'); 45 | return populatePropertyElement(propertyBlock, `${heading}.`, text); 46 | } 47 | 48 | export function createHtmlLegendaryPropertyBlock(heading, text) { 49 | const propertyBlock = document.createElement('legendary-property-block'); 50 | return populatePropertyElement(propertyBlock, `${heading}.`, text); 51 | } 52 | 53 | function populatePropertyElement(element, heading, text) { 54 | const headingElement = document.createElement('h4'); 55 | const textElement = document.createElement('p'); 56 | 57 | headingElement.textContent = heading; 58 | textElement.innerHTML = text; 59 | 60 | element.appendChild(headingElement); 61 | element.appendChild(textElement); 62 | 63 | return element; 64 | } 65 | 66 | export function createMarkdownPropertyLine(heading, text) { 67 | return `> - **${heading}** ${text}`; 68 | } 69 | 70 | export function createMarkdownPropertyBlock(heading, text) { 71 | return `> ***${heading}.*** ${text}`; 72 | } -------------------------------------------------------------------------------- /src/js/helpers/file-helpers.js: -------------------------------------------------------------------------------- 1 | import isRunningInJsdom from './is-running-in-jsdom.js'; 2 | 3 | export async function fetchFromFile(path) { 4 | 5 | // fetch() isn't available in a Node environment, so use readFile() instead. 6 | 7 | if (isRunningInJsdom) { 8 | const fs = require('fs'); 9 | const util = require('util'); 10 | const readFile = util.promisify(fs.readFile); 11 | 12 | return await readFile(path).then(buffer => buffer.toString()); 13 | } else { 14 | return await fetch(path).then(stream => stream.text()); 15 | } 16 | } -------------------------------------------------------------------------------- /src/js/helpers/html-export-document-factory.js: -------------------------------------------------------------------------------- 1 | import { fetchFromFile } from './file-helpers.js'; 2 | 3 | let doc = null; 4 | 5 | export async function init() { 6 | const template = await fetchFromFile('src/html/export-inlined.html'); 7 | 8 | const parser = new DOMParser(); 9 | doc = parser.parseFromString(template, 'text/html'); 10 | 11 | return doc; 12 | } 13 | 14 | export function createInstance() { 15 | return doc.cloneNode(true); 16 | } -------------------------------------------------------------------------------- /src/js/helpers/html-templates.js: -------------------------------------------------------------------------------- 1 | import { fetchFromFile } from './file-helpers.js'; 2 | 3 | class HtmlTemplates { 4 | constructor() { 5 | this.templates = new Map(); 6 | } 7 | 8 | async addTemplate(name, path) { 9 | const content = await fetchFromFile(path); 10 | this.templates.set(name, content); 11 | } 12 | 13 | hasTemplate(name) { 14 | return this.templates.has(name); 15 | } 16 | 17 | getTemplate(name) { 18 | return this.templates.get(name); 19 | } 20 | } 21 | 22 | export default new HtmlTemplates(); 23 | -------------------------------------------------------------------------------- /src/js/helpers/is-running-in-jsdom.js: -------------------------------------------------------------------------------- 1 | const IsRunningInJsdom = navigator.userAgent.includes('jsdom'); 2 | export default IsRunningInJsdom; -------------------------------------------------------------------------------- /src/js/helpers/local-storage-proxy.js: -------------------------------------------------------------------------------- 1 | class LocalStorageProxy { 2 | constructor() { 3 | this.jsonKey = 'json'; 4 | this.localSettingsKey = 'localSettings'; 5 | } 6 | 7 | loadJson() { 8 | return localStorage.getItem(this.jsonKey); 9 | } 10 | 11 | saveJson(json) { 12 | localStorage.setItem(this.jsonKey, json); 13 | } 14 | 15 | clearJson() { 16 | localStorage.removeItem(this.jsonKey); 17 | } 18 | 19 | loadLocalSettings() { 20 | return localStorage.getItem(this.localSettingsKey); 21 | } 22 | 23 | saveLocalSettings(localSettings) { 24 | localStorage.setItem(this.localSettingsKey, localSettings); 25 | } 26 | 27 | clearLocalSettings() { 28 | localStorage.removeItem(this.localSettingsKey); 29 | } 30 | } 31 | 32 | export default new LocalStorageProxy(); -------------------------------------------------------------------------------- /src/js/helpers/number-helpers.js: -------------------------------------------------------------------------------- 1 | export function convertToInteger(string) { 2 | const integer = parseInt(string, 10); 3 | return isNaN(integer) ? null : integer; 4 | } 5 | 6 | export function formatIntegerWithCommas(integer) { 7 | return integer.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); 8 | } -------------------------------------------------------------------------------- /src/js/helpers/object-helpers.js: -------------------------------------------------------------------------------- 1 | export function copyObjectProperties(target, source) { 2 | for (const propertyName in source) { 3 | let propertyDescriptor = Object.getOwnPropertyDescriptor(source, propertyName); 4 | Object.defineProperty(target, propertyName, propertyDescriptor); 5 | } 6 | } -------------------------------------------------------------------------------- /src/js/helpers/print-helpers.js: -------------------------------------------------------------------------------- 1 | export default function printHtml(content) { 2 | const frame = createFrame(content); 3 | document.body.appendChild(frame); 4 | } 5 | 6 | function createFrame(content) { 7 | const frame = document.createElement('iframe'); 8 | 9 | frame.addEventListener('load', onLoadFrame); 10 | frame.style.position = 'fixed'; 11 | frame.style.right = '0'; 12 | frame.style.bottom = '0'; 13 | frame.style.width = '0'; 14 | frame.style.height = '0'; 15 | frame.style.border = '0'; 16 | frame.srcdoc = content; 17 | 18 | return frame; 19 | } 20 | 21 | function onLoadFrame() { 22 | this.contentWindow.__container__ = this; 23 | this.contentWindow.addEventListener('beforeunload', onUnloadFrame); 24 | this.contentWindow.addEventListener('afterprint', onUnloadFrame); 25 | this.contentWindow.focus(); 26 | this.contentWindow.print(); 27 | } 28 | 29 | function onUnloadFrame() { 30 | document.body.removeChild(this.__container__); 31 | } -------------------------------------------------------------------------------- /src/js/helpers/sanitize-html.js: -------------------------------------------------------------------------------- 1 | const createDOMPurify = require('dompurify'); 2 | const DOMPurify = createDOMPurify(window); 3 | 4 | export default function sanitizeHTML(string) { 5 | const options = { 6 | ALLOWED_TAGS: ['strong', 'em', 'b', 'i'] 7 | }; 8 | return DOMPurify.sanitize(string, options); 9 | } -------------------------------------------------------------------------------- /src/js/helpers/spell-helpers.js: -------------------------------------------------------------------------------- 1 | import { capitalizeFirstLetter } from './string-formatter.js'; 2 | 3 | export function getSpellDescription(spell) { 4 | const school = capitalizeFirstLetter(spell.school); 5 | const levelAndSchool = (spell.level === 0) ? `${school} Cantrip` : `Lvl ${spell.level} ${school}`; 6 | const ritual = spell.ritual ? ' (ritual)' : ''; 7 | const castingTime = capitalizeFirstLetter(spell.castingTime); 8 | const range = capitalizeFirstLetter(spell.range); 9 | const components = getSpellComponents(spell); 10 | const concentrationAndDuration = spell.concentration ? `Concentration, ${spell.duration}` : capitalizeFirstLetter(spell.duration); 11 | 12 | return `${levelAndSchool}${ritual}; ${castingTime}; ${range}; ${concentrationAndDuration}; ${components}`; 13 | } 14 | 15 | function getSpellComponents(spell) { 16 | const components = []; 17 | if (spell.components.verbal) components.push('Verbal'); 18 | if (spell.components.somatic) components.push('Somatic'); 19 | if (spell.components.material) components.push('Material'); 20 | 21 | return components.join(', '); 22 | } -------------------------------------------------------------------------------- /src/js/helpers/string-formatter.js: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | 5 | export function escapeHtml(string) { 6 | return string 7 | .replace(/&/g, '&') 8 | .replace(//g, '>'); 10 | } 11 | 12 | export function formatModifier(modifier) { 13 | const operator = formatModifierOperator(modifier); 14 | const number = formatModifierNumber(modifier); 15 | 16 | return `${operator}${number}`; 17 | } 18 | 19 | export function formatModifierOperator(modifier) { 20 | if (modifier < 0) { 21 | // This is an EN dash (U+2013). 22 | // This stands out more than a normal minus sign. 23 | return '–'; 24 | } 25 | return '+'; 26 | } 27 | 28 | export function formatModifierNumber(modifier) { 29 | if (modifier < 0) { 30 | return Math.abs(modifier).toString(); 31 | } 32 | return modifier.toString(); 33 | } 34 | 35 | export function formatIntegerWithOrdinalIndicator(integer) { 36 | const ordinals = ['st', 'nd', 'rd', 'th']; 37 | 38 | const lastTwoDigits = integer % 100; 39 | const lastDigit = integer % 10; 40 | 41 | const endsWithElevenToNineteen = lastTwoDigits >= 11 && lastTwoDigits <= 19; 42 | const endsWithOneTwoOrThree = lastDigit >= 1 && lastDigit <= 3; 43 | 44 | const ordinal = (!endsWithElevenToNineteen && endsWithOneTwoOrThree) ? ordinals[lastDigit - 1] : ordinals[3]; 45 | 46 | return `${integer}${ordinal}`; 47 | } 48 | 49 | export function formatSpellSlotQuantity(spellSlotQuantity) { 50 | return (spellSlotQuantity === 1) ? `${spellSlotQuantity} slot` : `${spellSlotQuantity} slots`; 51 | } 52 | 53 | export function trimTrailingPeriods(string) { 54 | return string.replace(/\.+$/, ''); 55 | } 56 | 57 | export function nullIfEmptyString(string) { 58 | return string === '' ? null : string; 59 | } 60 | -------------------------------------------------------------------------------- /src/js/helpers/test/event-interceptor.js: -------------------------------------------------------------------------------- 1 | export default class EventInterceptor { 2 | constructor(eventTarget, eventType) { 3 | this.event = null; 4 | 5 | eventTarget.addEventListener(eventType, (event) => { 6 | this.event = event; 7 | }); 8 | } 9 | 10 | popEvent() { 11 | const event = this.event; 12 | this.event = null; 13 | return event; 14 | } 15 | } -------------------------------------------------------------------------------- /src/js/helpers/test/expect-matchers.js: -------------------------------------------------------------------------------- 1 | import toBeSelected from './matchers/to-be-selected.js'; 2 | import toBeInMode from './matchers/to-be-in-mode.js'; 3 | import toHaveError from './matchers/to-have-error.js'; 4 | import toHaveElementsEnabledOrDisabledBasedOnCheckboxState from './matchers/to-have-elements-enabled-or-disabled-based-on-checkbox-state.js'; 5 | import { toShowPropertyLine, toExportPropertyLineToHtml, toExportPropertyLineToMarkdown, toBeHtmlPropertyBlock } from './matchers/property-line-matchers.js'; 6 | 7 | // JSDOM 15.2.0 or higher can only focus elements that are added to the document body. It cannot focus on elements in a shadow root. 8 | // TODO: Remove this matcher when JSDOM allows us to focus on elements in a shadow root. 9 | // eslint-disable-next-line no-unused-vars 10 | const toHaveFocus = function(element) { 11 | return { 12 | pass: true 13 | }; 14 | }; 15 | 16 | expect.extend({ 17 | toHaveFocus, 18 | toBeSelected, 19 | toBeInMode, 20 | toHaveError, 21 | toHaveElementsEnabledOrDisabledBasedOnCheckboxState, 22 | toShowPropertyLine, 23 | toExportPropertyLineToHtml, 24 | toExportPropertyLineToMarkdown, 25 | toBeHtmlPropertyBlock 26 | }); -------------------------------------------------------------------------------- /src/js/helpers/test/matchers/to-be-in-mode.js: -------------------------------------------------------------------------------- 1 | export default function toBeInMode(section, expectedMode) { 2 | const sectionHiddenClass = 'section_hidden'; 3 | 4 | const showSectionClassList = section.showElements.section.classList; 5 | const editSectionClassList = section.editElements.section.classList; 6 | 7 | const hasMatchingMode = (section.mode === expectedMode); 8 | let showSectionHasHiddenClass = showSectionClassList.contains(sectionHiddenClass); 9 | let editSectionHasHiddenClass = editSectionClassList.contains(sectionHiddenClass); 10 | 11 | if (expectedMode === 'show') { 12 | showSectionHasHiddenClass = ! showSectionHasHiddenClass; 13 | } else if(expectedMode === 'edit') { 14 | editSectionHasHiddenClass = ! editSectionHasHiddenClass; 15 | } else if(expectedMode !== 'hidden') { 16 | throw new Error(`'${expectedMode}' is not a valid section mode.`); 17 | } 18 | 19 | let message = ''; 20 | let pass = hasMatchingMode && 21 | showSectionHasHiddenClass && 22 | editSectionHasHiddenClass; 23 | 24 | if (this.isNot && hasMatchingMode) { 25 | message += `expected section mode not to be '${expectedMode}', but was '${section.mode}'\n`; 26 | } else if (! this.isNot && ! hasMatchingMode) { 27 | message += `expected section mode to be '${expectedMode}', but was '${section.mode}'\n`; 28 | } 29 | 30 | if (this.isNot && showSectionHasHiddenClass) { 31 | message += `expected show section not to have '${sectionHiddenClass}' class, but was '${showSectionClassList.value}'\n`; 32 | } else if (! this.isNot && ! showSectionHasHiddenClass) { 33 | message += `expected show section to have '${sectionHiddenClass}' class, but was '${showSectionClassList.value}'\n`; 34 | } 35 | 36 | if (this.isNot && editSectionHasHiddenClass) { 37 | message += `expected edit section classes not to have '${sectionHiddenClass}' class, but was '${editSectionClassList.value}'`; 38 | } else if (! this.isNot && ! editSectionHasHiddenClass) { 39 | message += `expected edit section classes to have '${sectionHiddenClass}' class, but was '${editSectionClassList.value}'`; 40 | } 41 | 42 | return { 43 | message: () => message, 44 | pass: pass 45 | }; 46 | } -------------------------------------------------------------------------------- /src/js/helpers/test/matchers/to-be-selected.js: -------------------------------------------------------------------------------- 1 | export default function toBeSelected(element) { 2 | const pass = isTextSelected(element); 3 | let message = null; 4 | 5 | if (pass) { 6 | message = 'expected element not to be selected, but was selected'; 7 | } else { 8 | message = 'expected element to be selected'; 9 | } 10 | 11 | return { 12 | pass: pass, 13 | message: () => message 14 | }; 15 | } 16 | 17 | function isTextSelected(element) { 18 | if (typeof element.selectionStart === 'number') { 19 | return element.selectionStart === 0 && element.selectionEnd === element.value.length; 20 | } else if (typeof document.selection !== 'undefined') { 21 | return document.selection.createRange().text === element.value; 22 | } else { 23 | throw 'Unable to determine the selected text for the element using JSDOM.'; 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/helpers/test/matchers/to-have-elements-enabled-or-disabled-based-on-checkbox-state.js: -------------------------------------------------------------------------------- 1 | export default function toHaveElementsEnabledOrDisabledBasedOnCheckboxState( 2 | section, 3 | checkbox, 4 | namesOfElementsEnabledWhenChecked, 5 | namesOfElementsDisabledWhenChecked) { 6 | if (this.isNot) { 7 | throw new Error('The matcher toHaveElementsEnabledOrDisabledBasedOnCheckboxState cannot be used with the not modifier.'); 8 | } 9 | 10 | let messages = []; 11 | 12 | for (const elementName of namesOfElementsEnabledWhenChecked) { 13 | if (section.editElements[elementName].hasAttribute('disabled') === checkbox.checked) { 14 | messages.push(`expected ${elementName} to be ${getEnabledDisabledText(checkbox.checked)}, but was ${getEnabledDisabledText(! checkbox.checked)}`); 15 | } 16 | } 17 | 18 | for (const elementName of namesOfElementsDisabledWhenChecked) { 19 | if (section.editElements[elementName].hasAttribute('disabled') !== checkbox.checked) { 20 | messages.push(`expected ${elementName} to be ${getEnabledDisabledText(! checkbox.checked)}, but was ${getEnabledDisabledText(checkbox.checked)}`); 21 | } 22 | } 23 | 24 | return { 25 | message: () => messages.join('\n'), 26 | pass: (messages.length === 0) 27 | }; 28 | } 29 | 30 | function getEnabledDisabledText(boolean) { 31 | return (boolean ? 'enabled' : 'disabled'); 32 | } -------------------------------------------------------------------------------- /src/js/helpers/test/matchers/to-have-error.js: -------------------------------------------------------------------------------- 1 | export default function toHaveError(section, expectedFieldElement, expectedMessage, expectedIndex = 0) { 2 | if (this.isNot) { 3 | throw new Error('The matcher toHaveError cannot be used with the not modifier.'); 4 | } 5 | 6 | const errors = section.errorMessages.errors; 7 | 8 | const errorExists = (errors.length - 1 >= expectedIndex); 9 | if (! errorExists) { 10 | return { 11 | message: () => `expected error with index ${expectedIndex} to exist, but only ${errors.length} errors exist`, 12 | pass: false 13 | }; 14 | } 15 | 16 | const theError = errors[expectedIndex]; 17 | 18 | // JSDOM 15.2.0 or higher can only focus elements that are added to the document body. It cannot focus on elements in a shadow root. 19 | // TODO: Re-enable checking elements for focus when JSDOM allows us to focus on elements in a shadow root. 20 | // const focusedElement = theError.fieldElement.ownerDocument.activeElement; 21 | 22 | const hasMatchingFieldElement = (theError.fieldElement === expectedFieldElement); 23 | const hasMatchingMessage = (theError.message === expectedMessage); 24 | // const fieldElementHasFocus = (focusedElement === expectedFieldElement); 25 | 26 | let message = ''; 27 | let pass = false; 28 | 29 | pass = (hasMatchingFieldElement && hasMatchingMessage); 30 | 31 | // if (expectedIndex == 0) { 32 | // pass = (pass && fieldElementHasFocus); 33 | // } 34 | 35 | if (! hasMatchingFieldElement) { 36 | message += `expected error element to be '${expectedFieldElement.id}', but was '${theError.fieldElement.id}'\n`; 37 | } 38 | if (! hasMatchingMessage) { 39 | message += `expected error message to be '${expectedMessage}', but was '${theError.message}'`; 40 | } 41 | // if (expectedIndex === 0 && ! fieldElementHasFocus) { 42 | // message += `expected error element '${expectedFieldElement.id}' to have focus, but was '${focusedElement.id}'\n`; 43 | // } 44 | 45 | return { 46 | message: () => message, 47 | pass: pass 48 | }; 49 | } -------------------------------------------------------------------------------- /src/js/helpers/test/test-custom-elements.js: -------------------------------------------------------------------------------- 1 | import BlockTextArea from '../../elements/builtin/block-textarea.js'; 2 | import DynamicSelect from '../../elements/builtin/dynamic-select.js'; 3 | import EnableDisableElementsCheckbox from '../../elements/builtin/enable-disable-elements-checkbox.js'; 4 | import NumberInput from '../../elements/builtin/number-input.js'; 5 | import NumberSelect from '../../elements/builtin/number-select.js'; 6 | import PropertyDataList from '../../elements/builtin/property-datalist.js'; 7 | import SanitizedParagraph from '../../elements/builtin/sanitized-paragraph.js'; 8 | import TextInput from '../../elements/builtin/text-input.js'; 9 | 10 | import ErrorMessages from '../../elements/autonomous/error-messages.js'; 11 | import ExpressionMenu from '../../elements/autonomous/menus/expression-menu.js'; 12 | import SlideToggle from '../../elements/autonomous/slide-toggle.js'; 13 | import PropertyList from '../../elements/autonomous/lists/property-list.js'; 14 | import PropertyListItem from '../../elements/autonomous/lists/property-list-item.js'; 15 | import DisplayBlockList from '../../elements/autonomous/lists/display-block-list.js'; 16 | import DisplayBlock from '../../elements/autonomous/lists/display-block.js'; 17 | import EditableBlockList from '../../elements/autonomous/lists/editable-block-list.js'; 18 | import EditableBlock from '../../elements/autonomous/lists/editable-block.js'; 19 | 20 | export async function define() { 21 | const customElements = [ 22 | BlockTextArea, 23 | DynamicSelect, 24 | EnableDisableElementsCheckbox, 25 | NumberInput, 26 | NumberSelect, 27 | PropertyDataList, 28 | SanitizedParagraph, 29 | TextInput, 30 | 31 | ErrorMessages, 32 | ExpressionMenu, 33 | SlideToggle, 34 | PropertyList, 35 | PropertyListItem, 36 | DisplayBlockList, 37 | DisplayBlock, 38 | EditableBlockList, 39 | EditableBlock 40 | ]; 41 | 42 | for (const element of customElements) { 43 | await element.define(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/js/helpers/test/test-globals.js: -------------------------------------------------------------------------------- 1 | const createDOMPurify = require('dompurify'); 2 | global.DOMPurify = createDOMPurify(window); 3 | 4 | global.ClipboardJS = require('clipboard'); 5 | 6 | // Mock for html_beautify should do nothing to the content 7 | global.html_beautify = function(content) { 8 | return content; 9 | }; 10 | 11 | global.waitForExpect = require('wait-for-expect'); -------------------------------------------------------------------------------- /src/js/models/challenge-rating.js: -------------------------------------------------------------------------------- 1 | import PropertyLineModel from './property-line-model.js'; 2 | 3 | import ChallengeRatingToExperiencePoints from '../data/challenge-rating-to-experience-points.js'; 4 | import ChallengeRatingToProficiencyBonus from '../data/challenge-rating-to-proficiency-bonus.js'; 5 | 6 | import { formatIntegerWithCommas } from '../helpers/number-helpers.js'; 7 | 8 | export default class ChallengeRating extends PropertyLineModel { 9 | constructor() { 10 | super('Challenge'); 11 | 12 | this.reset(); 13 | } 14 | 15 | reset() { 16 | this.challengeRating = '0'; 17 | this.experiencePoints = 10; 18 | this.proficiencyBonus = 2; 19 | } 20 | 21 | get jsonPropertyNames() { 22 | return [ 23 | 'challengeRating', 24 | 'experiencePoints', 25 | 'proficiencyBonus' 26 | ]; 27 | } 28 | 29 | updateExperiencePointsAndProficiencyBonusFromChallengeRating() { 30 | this.experiencePoints = ChallengeRatingToExperiencePoints[this.challengeRating]; 31 | this.proficiencyBonus = ChallengeRatingToProficiencyBonus[this.challengeRating]; 32 | } 33 | 34 | get text() { 35 | const formattedExperiencePoints = formatIntegerWithCommas(this.experiencePoints); 36 | return `${this.challengeRating} (${formattedExperiencePoints} XP)`; 37 | } 38 | 39 | get htmlText() { 40 | return this.text; 41 | } 42 | 43 | toParserOptions() { 44 | return this.proficiencyBonus; 45 | } 46 | 47 | fromOpen5e(json) { 48 | this.reset(); 49 | 50 | this.challengeRating = json['challenge_rating']; 51 | this.updateExperiencePointsAndProficiencyBonusFromChallengeRating(); 52 | } 53 | } -------------------------------------------------------------------------------- /src/js/models/current-context.js: -------------------------------------------------------------------------------- 1 | import Creature from './creature.js'; 2 | import LayoutSettings from './settings/layout-settings.js'; 3 | import LocalSettings from './settings/local-settings.js'; 4 | 5 | class CurrentContext { 6 | constructor() { 7 | this.creature = new Creature(); 8 | this.layoutSettings = new LayoutSettings(); 9 | this.localSettings = new LocalSettings(); 10 | } 11 | 12 | reset() { 13 | this.creature.reset(); 14 | this.layoutSettings.reset(); 15 | } 16 | } 17 | 18 | export default new CurrentContext(); -------------------------------------------------------------------------------- /src/js/models/lists/block/actions.js: -------------------------------------------------------------------------------- 1 | import BlockListModel from './block-list-model.js'; 2 | import BlockModel from './block-model.js'; 3 | import isRunningInJsdom from '../../../helpers/is-running-in-jsdom.js'; 4 | 5 | export default class Actions extends BlockListModel { 6 | constructor() { 7 | super('Actions', 8 | 'Action', 9 | 'actions'); 10 | } 11 | 12 | reset() { 13 | 14 | // For tests, start with no action blocks so it is easy for tests to populate the data they need. 15 | // For production, start with the Club attack action as part of the Commoner statblock. 16 | 17 | if (isRunningInJsdom) { 18 | this.blocks = []; 19 | } else { 20 | const club = new BlockModel( 21 | 'Club', 22 | '*Melee Weapon Attack:* ATK[STR] to hit, reach 5 ft., one target. *Hit:* DMG[1d4 + STR] bludgeoning damage.', 23 | '*Melee Weapon Attack:* +2 to hit, reach 5 ft., one target. *Hit:* 2 (1d4) bludgeoning damage.', 24 | 'Melee Weapon Attack: +2 to hit, reach 5 ft., one target. Hit: 2 (1d4) bludgeoning damage.' 25 | ); 26 | this.blocks = [club]; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/js/models/lists/block/block-model.js: -------------------------------------------------------------------------------- 1 | import * as ExportHelpers from '../../../helpers/export-helpers.js'; 2 | 3 | export default class BlockModel { 4 | constructor(name, text, markdownText = '', htmlText = '') { 5 | this.name = name; 6 | this.text = text; 7 | this.markdownText = markdownText; 8 | this.htmlText = htmlText; 9 | } 10 | 11 | toJson() { 12 | return { 13 | name: this.name, 14 | text: this.text 15 | }; 16 | } 17 | 18 | toHtml() { 19 | return ExportHelpers.createHtmlPropertyBlock(this.name, this.htmlText); 20 | } 21 | 22 | toMarkdown() { 23 | return ExportHelpers.createMarkdownPropertyBlock(this.name, this.markdownText); 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/models/lists/block/legendary-actions.js: -------------------------------------------------------------------------------- 1 | import BlockListModel from './block-list-model.js'; 2 | import LegendaryBlockModel from './legendary-block-model.js'; 3 | 4 | export default class LegendaryActions extends BlockListModel { 5 | constructor() { 6 | super('Legendary Actions', 7 | 'Legendary Action', 8 | 'legendary_actions', 9 | LegendaryBlockModel); 10 | } 11 | 12 | reset() { 13 | super.reset(); 14 | 15 | this.resetDescription(); 16 | } 17 | 18 | resetDescription() { 19 | this.description = '[NAME] can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature\'s turn. [NAME] regains spent legendary actions at the start of its turn.'; 20 | this.markdownDescription = ''; 21 | this.htmlDescription = ''; 22 | } 23 | 24 | fromOpen5e(json) { 25 | super.fromOpen5e(json); 26 | 27 | this.resetDescription(); 28 | 29 | const legendaryDesc = json['legendary_desc']; 30 | if (legendaryDesc !== '') { 31 | this.description = legendaryDesc; 32 | } 33 | } 34 | 35 | fromJson(json) { 36 | super.fromJson(json); 37 | this.description = json.description; 38 | } 39 | 40 | toJson() { 41 | const json = super.toJson(); 42 | json.description = this.description; 43 | return json; 44 | } 45 | 46 | toHtml() { 47 | const fragment = super.toHtml(); 48 | 49 | const firstBlockElement = fragment.querySelector('legendary-property-block'); 50 | 51 | const descriptionElement = document.createElement('p'); 52 | descriptionElement.innerHTML = this.htmlDescription; 53 | fragment.insertBefore(descriptionElement, firstBlockElement); 54 | 55 | return fragment; 56 | } 57 | 58 | toMarkdown() { 59 | const heading = `> ### ${this.headingName}\n`; 60 | const description = `> ${this.markdownDescription}\n`; 61 | const markdownBlocks = 62 | this.blocks.map(block => block.toMarkdown()); 63 | const markdownBlocksAsText = 64 | markdownBlocks.join('\n>\n'); 65 | 66 | return `${heading}${description}${markdownBlocksAsText}`; 67 | } 68 | } -------------------------------------------------------------------------------- /src/js/models/lists/block/legendary-block-model.js: -------------------------------------------------------------------------------- 1 | import BlockModel from './block-model.js'; 2 | import * as ExportHelpers from '../../../helpers/export-helpers.js'; 3 | 4 | export default class LegendaryBlockModel extends BlockModel { 5 | constructor(name, text, markdownText, htmlText) { 6 | super(name, text, markdownText, htmlText); 7 | } 8 | 9 | toHtml() { 10 | return ExportHelpers.createHtmlLegendaryPropertyBlock(this.name, this.htmlText); 11 | } 12 | } -------------------------------------------------------------------------------- /src/js/models/lists/block/reactions.js: -------------------------------------------------------------------------------- 1 | import BlockListModel from './block-list-model.js'; 2 | 3 | export default class Reactions extends BlockListModel { 4 | constructor() { 5 | super('Reactions', 6 | 'Reaction', 7 | 'reactions'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/js/models/lists/block/special-traits.js: -------------------------------------------------------------------------------- 1 | import BlockListModel from './block-list-model.js'; 2 | 3 | export default class SpecialTraits extends BlockListModel { 4 | constructor() { 5 | super(null, 6 | 'Special Trait', 7 | 'special_abilities'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/js/models/lists/property/condition-immunities.js: -------------------------------------------------------------------------------- 1 | import PropertyListModel from './property-list-model.js'; 2 | 3 | export default class ConditionImmunities extends PropertyListModel { 4 | constructor() { 5 | super('Condition Immunities', 6 | 'Condition Immunity', 7 | 'condition_immunities'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/js/models/lists/property/damage-immunities.js: -------------------------------------------------------------------------------- 1 | import PropertyListModel from './property-list-model.js'; 2 | 3 | export default class DamageImmunities extends PropertyListModel { 4 | constructor() { 5 | super('Damage Immunities', 6 | 'Damage Immunity', 7 | 'damage_immunities'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/js/models/lists/property/damage-resistances.js: -------------------------------------------------------------------------------- 1 | import PropertyListModel from './property-list-model.js'; 2 | 3 | export default class DamageResistances extends PropertyListModel { 4 | constructor() { 5 | super('Damage Resistances', 6 | 'Damage Resistance', 7 | 'damage_resistances'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/js/models/lists/property/damage-vulnerabilities.js: -------------------------------------------------------------------------------- 1 | import PropertyListModel from './property-list-model.js'; 2 | 3 | export default class DamageVulnerabilities extends PropertyListModel { 4 | constructor() { 5 | super('Damage Vulnerabilities', 6 | 'Damage Vulnerability', 7 | 'damage_vulnerabilities'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/js/models/lists/property/languages.js: -------------------------------------------------------------------------------- 1 | import PropertyListModel from './property-list-model.js'; 2 | import { convertToInteger } from '../../../helpers/number-helpers.js'; 3 | 4 | export default class Languages extends PropertyListModel { 5 | constructor() { 6 | super('Languages', 7 | 'Language', 8 | 'languages'); 9 | } 10 | 11 | reset() { 12 | this.clear(); 13 | this.items = ['any one language (usually Common)']; 14 | } 15 | 16 | clear() { 17 | super.clear(); 18 | this.telepathy = null; 19 | } 20 | 21 | get text() { 22 | let text = super.text; 23 | 24 | if (this.telepathy !== null) { 25 | if (text !== '') { 26 | text += ', '; 27 | } 28 | 29 | text += `telepathy ${this.telepathy} ft.`; 30 | } 31 | 32 | if (text === '') { 33 | // This is an EM dash (U+2014). 34 | // This appears significantly wider than a normal dash. 35 | text = '—'; 36 | } 37 | 38 | return text; 39 | } 40 | 41 | fromOpen5e(json) { 42 | super.fromOpen5e(json); 43 | 44 | const regex = /(?<=^telepathy )\d+(?= ft\.$)/; 45 | const telepathyItemIndex = this.items.findIndex(item => regex.test(item)); 46 | 47 | if (telepathyItemIndex >= 0) { 48 | const removedItems = this.items.splice(telepathyItemIndex, 1); 49 | const telepathyMatches = removedItems[0].match(regex); 50 | const telepathyValue = telepathyMatches[0]; 51 | this.telepathy = convertToInteger(telepathyValue); 52 | } 53 | } 54 | 55 | fromJson(json) { 56 | super.fromJson(json); 57 | 58 | this.telepathy = json.telepathy; 59 | } 60 | 61 | toJson() { 62 | const json = super.toJson(); 63 | json.telepathy = this.telepathy; 64 | return json; 65 | } 66 | } -------------------------------------------------------------------------------- /src/js/models/model.js: -------------------------------------------------------------------------------- 1 | export default class Model { 2 | constructor() { 3 | } 4 | 5 | reset() { 6 | throw new Error( 7 | `The class '${this.constructor.name}' must implement the reset() method.`); 8 | } 9 | 10 | get jsonPropertyNames() { 11 | return []; 12 | } 13 | 14 | // eslint-disable-next-line no-unused-vars 15 | fromOpen5e(json) { 16 | throw new Error( 17 | `The class '${this.constructor.name}' must implement the fromOpen5e() method.`); 18 | } 19 | 20 | fromJson(json) { 21 | this.reset(); 22 | 23 | for (const propertyName of this.jsonPropertyNames) { 24 | if (propertyName in json) { 25 | this[propertyName] = json[propertyName]; 26 | } 27 | } 28 | } 29 | 30 | toJson() { 31 | const json = {}; 32 | for (const propertyName of this.jsonPropertyNames) { 33 | if (propertyName in this) { 34 | json[propertyName] = this[propertyName]; 35 | } 36 | } 37 | return json; 38 | } 39 | 40 | toHtml() { 41 | throw new Error( 42 | `The class '${this.constructor.name}' must implement the toHtml() method.`); 43 | } 44 | 45 | toMarkdown() { 46 | throw new Error( 47 | `The class '${this.constructor.name}' must implement the toMarkdown() method.`); 48 | } 49 | } -------------------------------------------------------------------------------- /src/js/models/property-line-model.js: -------------------------------------------------------------------------------- 1 | import Model from './model.js'; 2 | import * as ExportHelpers from '../helpers/export-helpers.js'; 3 | 4 | export default class PropertyLineModel extends Model { 5 | constructor(headingName) { 6 | super(); 7 | 8 | this.headingName = headingName; 9 | } 10 | 11 | get text() { 12 | throw new Error( 13 | `The class '${this.constructor.name}' must implement the text() method.`); 14 | } 15 | 16 | get htmlText() { 17 | throw new Error( 18 | `The class '${this.constructor.name}' must implement the htmlText() method.`); 19 | } 20 | 21 | toHtml() { 22 | return ExportHelpers.createHtmlPropertyLine(this.headingName, this.htmlText); 23 | } 24 | 25 | toMarkdown() { 26 | return ExportHelpers.createMarkdownPropertyLine(this.headingName, this.text); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/js/models/settings/layout-settings.js: -------------------------------------------------------------------------------- 1 | export default class LayoutSettings { 2 | constructor() { 3 | this.reset(); 4 | } 5 | 6 | reset() { 7 | this.columns = 1; 8 | this.twoColumnMode = 'auto'; 9 | this.twoColumnHeight = 600; 10 | } 11 | 12 | fromJson(json) { 13 | this.columns = json.columns; 14 | this.twoColumnMode = json.twoColumnMode; 15 | this.twoColumnHeight = json.twoColumnHeight; 16 | } 17 | 18 | toJson() { 19 | return { 20 | columns: this.columns, 21 | twoColumnMode: this.twoColumnMode, 22 | twoColumnHeight: this.twoColumnHeight, 23 | }; 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/models/settings/local-settings.js: -------------------------------------------------------------------------------- 1 | // Contains settings that are saved in local storage, but not in JSON 2 | 3 | export default class LocalSettings { 4 | constructor() { 5 | this.reset(); 6 | } 7 | 8 | reset() { 9 | this.version = '0.0.0'; 10 | this.emptySectionsVisibility = true; 11 | this.gettingStartedVisibility = true; 12 | } 13 | } -------------------------------------------------------------------------------- /src/js/models/speed.js: -------------------------------------------------------------------------------- 1 | import PropertyLineModel from './property-line-model.js'; 2 | 3 | export default class Speed extends PropertyLineModel { 4 | constructor() { 5 | super('Speed'); 6 | 7 | this.reset(); 8 | } 9 | 10 | reset() { 11 | this.walk = 30; 12 | this.burrow = null; 13 | this.climb = null; 14 | this.fly = null; 15 | this.hover = false; 16 | this.swim = null; 17 | 18 | this.useCustomText = false; 19 | this.customText = ''; 20 | this.htmlCustomText = ''; 21 | } 22 | 23 | get jsonPropertyNames() { 24 | return [ 25 | 'walk', 26 | 'burrow', 27 | 'climb', 28 | 'fly', 29 | 'hover', 30 | 'swim', 31 | 'useCustomText', 32 | 'customText' 33 | ]; 34 | } 35 | 36 | get text() { 37 | if (this.useCustomText) { 38 | return this.customText; 39 | } 40 | 41 | return this.normalText; 42 | } 43 | 44 | get htmlText() { 45 | if (this.useCustomText) { 46 | return this.htmlCustomText; 47 | } 48 | 49 | return this.normalText; 50 | } 51 | 52 | get normalText() { 53 | const unit = 'ft.'; 54 | const list = []; 55 | const walk = (this.walk ? this.walk : 0); 56 | 57 | list.push(`${walk} ${unit}`); 58 | 59 | if (this.burrow != null) { 60 | list.push(`burrow ${this.burrow} ${unit}`); 61 | } 62 | if (this.climb != null) { 63 | list.push(`climb ${this.climb} ${unit}`); 64 | } 65 | if (this.fly != null) { 66 | const hover = (this.hover ? ' (hover)' : ''); 67 | list.push(`fly ${this.fly} ${unit}${hover}`); 68 | } 69 | if (this.swim != null) { 70 | list.push(`swim ${this.swim} ${unit}`); 71 | } 72 | 73 | return list.join(', '); 74 | } 75 | 76 | fromOpen5e(json) { 77 | this.reset(); 78 | 79 | if ('walk' in json.speed) { 80 | this.walk = json.speed.walk; 81 | } else { 82 | this.walk = null; 83 | } 84 | 85 | if ('burrow' in json.speed) this.burrow = json.speed.burrow; 86 | if ('climb' in json.speed) this.climb = json.speed.climb; 87 | if ('fly' in json.speed) this.fly = json.speed.fly; 88 | if ('hover' in json.speed) this.hover = json.speed.hover; 89 | if ('swim' in json.speed) this.swim = json.speed.swim; 90 | 91 | if ('notes' in json.speed) { 92 | this.useCustomText = true; 93 | this.customText = `${this.normalText} (${json.speed.notes})`; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/js/models/subtitle.js: -------------------------------------------------------------------------------- 1 | import Model from './model.js'; 2 | 3 | import creatureSizesToHitDieSizes from '../data/creature-sizes-to-hit-die-sizes.js'; 4 | import creatureTypes from '../data/creature-types.js'; 5 | import creatureAlignments from '../data/creature-alignments.js'; 6 | 7 | export default class Subtitle extends Model{ 8 | constructor() { 9 | super(); 10 | 11 | this.reset(); 12 | } 13 | 14 | reset() { 15 | this.size = 'Medium'; 16 | this.type = 'humanoid'; 17 | this.tags = 'any race'; 18 | this.alignment = 'any alignment'; 19 | 20 | this.useCustomText = false; 21 | this.customText = ''; 22 | } 23 | 24 | get jsonPropertyNames() { 25 | return [ 26 | 'size', 27 | 'type', 28 | 'tags', 29 | 'alignment', 30 | 'useCustomText', 31 | 'customText' 32 | ]; 33 | } 34 | 35 | get text() { 36 | if (this.useCustomText) { 37 | return this.customText; 38 | } 39 | 40 | return Subtitle.constructText(this.size, this.type, this.tags, this.alignment); 41 | } 42 | 43 | static constructText(size, type, tags, alignment) { 44 | if (tags === '') { 45 | return `${size} ${type}, ${alignment}`; 46 | } 47 | 48 | return `${size} ${type} (${tags}), ${alignment}`; 49 | } 50 | 51 | fromOpen5e(json) { 52 | this.reset(); 53 | 54 | const size = json.size; 55 | const type = json.type; 56 | const subtype = json.subtype.trim(); 57 | const alignment = json.alignment; 58 | 59 | // If the size, type, or alignment does not match the dropdown options, fallback to using custom text 60 | if (! (Object.keys(creatureSizesToHitDieSizes).includes(size) && 61 | creatureTypes.includes(type) && 62 | creatureAlignments.includes(alignment))) { 63 | this.useCustomText = true; 64 | this.customText = Subtitle.constructText(size, type, subtype, alignment); 65 | 66 | // Otherwise, set the size, type, tags, and alignment as normal 67 | } else { 68 | this.size = size; 69 | this.type = type; 70 | this.tags = subtype; 71 | this.alignment = alignment; 72 | } 73 | } 74 | 75 | toHtml() { 76 | const subtitleElement = document.createElement('h2'); 77 | subtitleElement.textContent = this.text; 78 | return subtitleElement; 79 | } 80 | 81 | toMarkdown() { 82 | return `>*${this.text}*`; 83 | } 84 | } -------------------------------------------------------------------------------- /src/js/models/title.js: -------------------------------------------------------------------------------- 1 | import Model from './model.js'; 2 | 3 | export default class Title extends Model { 4 | constructor() { 5 | super(); 6 | 7 | this.reset(); 8 | } 9 | 10 | reset() { 11 | this.fullName = 'Commoner'; 12 | this.shortName = ''; 13 | this.isProperNoun = false; 14 | } 15 | 16 | get jsonPropertyNames() { 17 | return [ 18 | 'fullName', 19 | 'shortName', 20 | 'isProperNoun' 21 | ]; 22 | } 23 | 24 | get grammaticalFullName() { 25 | return this.grammaticize(this.fullName); 26 | } 27 | 28 | get grammaticalShortName() { 29 | return this.grammaticize(this.shortName); 30 | } 31 | 32 | get grammaticalName() { 33 | return (this.shortName !== '') ? 34 | this.grammaticalShortName : 35 | this.grammaticalFullName; 36 | } 37 | 38 | grammaticize(name) { 39 | return (this.isProperNoun ? name : `the ${name.toLowerCase()}`); 40 | } 41 | 42 | fromOpen5e(json) { 43 | this.reset(); 44 | this.fullName = json.name; 45 | } 46 | 47 | toParserOptions() { 48 | return { 49 | name: this.grammaticalName, 50 | fullName: this.grammaticalFullName 51 | }; 52 | } 53 | 54 | toHtml() { 55 | const titleElement = document.createElement('h1'); 56 | titleElement.textContent = this.fullName; 57 | return titleElement; 58 | } 59 | 60 | toMarkdown() { 61 | return `> ## ${this.fullName}`; 62 | } 63 | } -------------------------------------------------------------------------------- /src/js/parsers/grammars/markdown-grammar.pegjs: -------------------------------------------------------------------------------- 1 | start 2 | = line:Line+ { return line.join(''); } 3 | / End { return ""; } 4 | 5 | Line 6 | = BlankLine 7 | / NormalLine 8 | 9 | BlankLine 10 | = NewLineChar 11 | 12 | NormalLine 13 | = inline:Inline+ end:EndOfLine { return `${inline.join('')}${end ? end : ''}`; } 14 | 15 | Inline 16 | = EscapedMarkdownChar 17 | / Markup 18 | / Text 19 | / Whitespace 20 | 21 | Markup 22 | = Strong 23 | / Emphasis 24 | 25 | Strong 26 | = StrongAsterisk 27 | / StrongUnderscore 28 | 29 | TwoAsteriskOpen 30 | = !EscapedMarkdownChar '**' 31 | 32 | TwoAsteriskClose 33 | = !EscapedMarkdownChar '**' 34 | 35 | StrongAsterisk 36 | = TwoAsteriskOpen 37 | inline:Inline+ 38 | TwoAsteriskClose 39 | { return `${inline.join('')}`; } 40 | 41 | TwoUnderlineOpen 42 | = !EscapedMarkdownChar '__' 43 | 44 | TwoUnderlineClose 45 | = !EscapedMarkdownChar '__' 46 | 47 | StrongUnderscore 48 | = TwoUnderlineOpen 49 | inline:Inline+ 50 | TwoUnderlineClose 51 | { return `${inline.join('')}`; } 52 | 53 | Emphasis 54 | = EmphasisAsterisk 55 | / EmphasisUnderscore 56 | 57 | OneAsteriskOpen 58 | = !EscapedMarkdownChar '*' 59 | 60 | OneAsteriskClose 61 | = !StrongAsterisk !EscapedMarkdownChar '*' 62 | 63 | EmphasisAsterisk 64 | = OneAsteriskOpen 65 | inline:Inline+ 66 | OneAsteriskClose 67 | { return `${inline.join('')}`; } 68 | 69 | OneUnderlineOpen 70 | = !EscapedMarkdownChar '_' 71 | 72 | OneUnderlineClose 73 | = !StrongUnderscore !EscapedMarkdownChar '_' 74 | 75 | EmphasisUnderscore 76 | = OneUnderlineOpen 77 | inline:Inline+ 78 | OneUnderlineClose 79 | { return `${inline.join('')}`; } 80 | 81 | Text 82 | = $(NormalChar+) 83 | 84 | Whitespace 85 | = $(SpaceChar+) 86 | 87 | EndOfLine 88 | = NewLineChar / End 89 | 90 | NormalChar 91 | = !( MarkdownChar / EscapedMarkdownChar / SpaceChar / NewLineChar ) . 92 | 93 | MarkdownChar 94 | = '*' / '_' 95 | 96 | EscapedMarkdownChar 97 | = '\\' char:MarkdownChar { return char; } 98 | 99 | NewLineChar 100 | = '\n' / $('\r' '\n'?) 101 | 102 | SpaceChar 103 | = ' ' / '\t' 104 | 105 | End 106 | = !. -------------------------------------------------------------------------------- /src/js/parsers/scripts/generate-parser-script.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // Run this script in node to generate the parser source code from the grammar. 4 | 5 | // Example usage from the "parsers" directory: 6 | // node scripts/generate-parser-script.js grammars/markdown-grammar.pegjs markdown-parser.js 7 | // node scripts/generate-parser-script.js grammars/math-grammar.pegjs math-parser.js 8 | // node scripts/generate-parser-script.js grammars/name-grammar.pegjs name-parser.js 9 | 10 | const fs = require('fs'); 11 | const peg = require('pegjs'); 12 | 13 | const grammarFileName = process.argv[2]; 14 | const parserFileName = process.argv[3]; 15 | 16 | const parserOptions = { 17 | format: 'bare', 18 | optimize: 'speed', 19 | output: 'source' 20 | }; 21 | 22 | console.log(`Reading grammar from '${grammarFileName}'...`); 23 | const grammar = fs.readFileSync(grammarFileName).toString(); 24 | console.log(' Done'); 25 | 26 | console.log('Generating parser from grammar...'); 27 | var parser = peg.generate(grammar, parserOptions); 28 | console.log(' Done'); 29 | 30 | // Inject 'export default' to the front of the parser object so that 31 | // it can be imported as part of an ES6 module. 32 | console.log('Converting parser into ES6 module...'); 33 | const indexOfObject = parser.indexOf('(function()'); 34 | parser = `${parser.slice(0, indexOfObject)}export default ${parser.slice(indexOfObject)}`; 35 | console.log(' Done'); 36 | 37 | console.log(`Writing parser to '${parserFileName}'...`); 38 | fs.writeFileSync(parserFileName, parser); 39 | console.log(' Done'); 40 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/js/init.js', 3 | output: { 4 | filename: 'app.bundle.js' 5 | }, 6 | optimization: { 7 | splitChunks: { 8 | name: false, 9 | cacheGroups: { 10 | commons: { 11 | test: /[\\/]node_modules[\\/]/, 12 | name: 'vendors', 13 | filename: '[name].bundle.js', 14 | chunks: 'all' 15 | } 16 | } 17 | } 18 | }, 19 | }; -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'eval-source-map', 7 | resolve: { 8 | fallback: { 9 | util: require.resolve('util') 10 | } 11 | } 12 | }); -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | module.exports = merge(common, { 8 | mode: 'production', 9 | devtool: false, 10 | plugins: [ 11 | // Uncomment to enable bundle size analysis 12 | // new BundleAnalyzerPlugin(), 13 | ], 14 | }); --------------------------------------------------------------------------------