From 3599b5fbc4232b775994d7f13f882e165a884640 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 26 Jul 2017 19:23:41 +0100 Subject: [PATCH 1/7] Added basic drag-and-drop functionality between spell lists Closes #105 --- rpg-docs/Model/Character/Spells.js | 81 +++++++++++++++++++ .../client/views/character/spells/spells.html | 4 +- .../client/views/character/spells/spells.js | 23 ++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/rpg-docs/Model/Character/Spells.js b/rpg-docs/Model/Character/Spells.js index 58f21cd2..c6036ee4 100644 --- a/rpg-docs/Model/Character/Spells.js +++ b/rpg-docs/Model/Character/Spells.js @@ -83,3 +83,84 @@ Spells.after.update(function (userId, spell, fieldNames) { Spells.allow(CHARACTER_SUBSCHEMA_ALLOW); Spells.deny(CHARACTER_SUBSCHEMA_DENY); + + + + +var checkMovePermission = function(spellId, parent) { + var spell = Spells.findOne(spellId); + if (!spell) + throw new Meteor.Error("No such spell", + "An spell could not be found to move"); + //handle permissions + var permission = Meteor.call("canWriteCharacter", spell.charId); + if (!permission){ + throw new Meteor.Error("Access denied", + "Not permitted to move spells from this character"); + } + if (parent.collection === "Characters"){ + permission = Meteor.call("canWriteCharacter", parent.id); + if (!permission){ + throw new Meteor.Error("Access denied", + "Not permitted to move spells to this character"); + } + } else { + var parentCollectionObject = global[parent.collection]; + var parentObject = null; + if (parentCollectionObject) + parentObject = parentCollectionObject.findOne( + parent.id, {fields: {_id: 1, charId: 1}} + ); + if (!parentObject) throw new Meteor.Error( + "Invalid parent", + "The destination parent " + parent.id + + " does not exist in the collection " + parent.collection + ); + if (parentObject.charId){ + permission = Meteor.call("canWriteCharacter", parentObject.charId); + if (!permission){ + throw new Meteor.Error("Access denied", + "Not permitted to move spells to this character"); + } + } + } +}; + +var moveSpell = function(spellId, parentCollection, parentId) { + var spell = Spells.findOne(spellId); + if (!spell) return; + parentCollection = parentCollection || spell.parent.collection; + parentId = parentId || spell.parent.id; + + if (Meteor.isServer) { + checkMovePermission(spellId, {collection: parentCollection, id: parentId}); + } + + + if (spell.parentCollection == "Characters") { //then we are moving the spell to a different character. + + } else { //else we are moving the spell within the same character + //update the spell provided the update will actually change something + if ( + spell.parent.collection !== parentCollection || + spell.parent.id !== parentId + ){ + Spells.update( + spellId, + {$set: { + "parent.collection": parentCollection, + "parent.id": parentId, + }} + ); + } + } +}; + + +Meteor.methods({ + moveSpellToList: function(spellId, spellListId) { + check(spellId, String); + check(spellListId, String); + moveSpell(spellId, "SpellLists", spellListId); + }, +}); \ No newline at end of file diff --git a/rpg-docs/client/views/character/spells/spells.html b/rpg-docs/client/views/character/spells/spells.html index 7158c8c3..b5011f82 100644 --- a/rpg-docs/client/views/character/spells/spells.html +++ b/rpg-docs/client/views/character/spells/spells.html @@ -83,7 +83,9 @@ {{#each spells ../_id ../../_id}} {{#if showSpell ../../_id}}
-
+
Date: Wed, 26 Jul 2017 19:54:18 +0100 Subject: [PATCH 2/7] CTRL-dragging now copies spells --- rpg-docs/Model/Character/Spells.js | 77 +++++++++++++------ .../client/views/character/spells/spells.js | 6 +- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/rpg-docs/Model/Character/Spells.js b/rpg-docs/Model/Character/Spells.js index c6036ee4..ae85cf5f 100644 --- a/rpg-docs/Model/Character/Spells.js +++ b/rpg-docs/Model/Character/Spells.js @@ -87,16 +87,20 @@ Spells.deny(CHARACTER_SUBSCHEMA_DENY); -var checkMovePermission = function(spellId, parent) { +var checkMovePermission = function(spellId, parent, destinationOnly) { var spell = Spells.findOne(spellId); if (!spell) throw new Meteor.Error("No such spell", "An spell could not be found to move"); //handle permissions - var permission = Meteor.call("canWriteCharacter", spell.charId); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move spells from this character"); + var permission; + + if (!destinationOnly) { //if we're not modifying the origin, only the destination + permission = Meteor.call("canWriteCharacter", spell.charId); + if (!permission){ + throw new Meteor.Error("Access denied", + "Not permitted to move spells from this character"); + } } if (parent.collection === "Characters"){ permission = Meteor.call("canWriteCharacter", parent.id); @@ -126,33 +130,55 @@ var checkMovePermission = function(spellId, parent) { } }; -var moveSpell = function(spellId, parentCollection, parentId) { +var moveSpell = function(spellId, parentCollection, parentId) { //moving spells between characters is NOT YET SUPPORTED :O var spell = Spells.findOne(spellId); if (!spell) return; parentCollection = parentCollection || spell.parent.collection; parentId = parentId || spell.parent.id; if (Meteor.isServer) { - checkMovePermission(spellId, {collection: parentCollection, id: parentId}); + checkMovePermission(spellId, {collection: parentCollection, id: parentId}, false); + } + + //update the spell provided the update will actually change something + if ( + spell.parent.collection !== parentCollection || + spell.parent.id !== parentId + ){ + Spells.update( + spellId, + {$set: { + "parent.collection": parentCollection, + "parent.id": parentId, + }} + ); + } +}; + +var copySpell = function(spellId, parentCollection, parentId) { + var spell = Spells.findOne(spellId); + if (!spell) return; + parentCollection = parentCollection || spell.parent.collection; + parentId = parentId || spell.parent.id; + + if (Meteor.isServer) { + checkMovePermission(spellId, {collection: parentCollection, id: parentId}, true); //we're only reading from the source character } - if (spell.parentCollection == "Characters") { //then we are moving the spell to a different character. - - } else { //else we are moving the spell within the same character - //update the spell provided the update will actually change something - if ( - spell.parent.collection !== parentCollection || - spell.parent.id !== parentId - ){ - Spells.update( - spellId, - {$set: { - "parent.collection": parentCollection, - "parent.id": parentId, - }} - ); - } + if (spell.parentCollection == "Characters") { //then we are copying the spell to a different character. + //TODO: handle this + } else { //else we are copying the spell within the same character + newSpell = _.clone(spell); + delete newSpell._id; + newSpellId = Spells.insert(newSpell); //add a new copy of the spell + Spells.update( + newSpellId, + {$set: { + "parent.collection": parentCollection, + "parent.id": parentId, + }} + ); } }; @@ -163,4 +189,9 @@ Meteor.methods({ check(spellListId, String); moveSpell(spellId, "SpellLists", spellListId); }, + copySpellToList: function(spellId, spellListId) { + check(spellId, String); + check(spellListId, String); + copySpell(spellId, "SpellLists", spellListId); + }, }); \ No newline at end of file diff --git a/rpg-docs/client/views/character/spells/spells.js b/rpg-docs/client/views/character/spells/spells.js index 45b1a54b..7b5bf170 100644 --- a/rpg-docs/client/views/character/spells/spells.js +++ b/rpg-docs/client/views/character/spells/spells.js @@ -352,7 +352,11 @@ Template.layout.events({ "drop .spellList": function(event, instance){ var spellId = event.originalEvent.dataTransfer.getData("dicecloud-id/spells"); //move spell to new list - Meteor.call("moveSpellToList", spellId, this._id); + if (event.ctrlKey){ + Meteor.call("copySpellToList", spellId, this._id); + } else { + Meteor.call("moveSpellToList", spellId, this._id); + } Session.set("inventory.dragSpellId", null); }, }); \ No newline at end of file From c76fe9914847ed8bfc72634e92517723ce1678d7 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 26 Jul 2017 21:02:36 +0100 Subject: [PATCH 3/7] Can now move and copy spells between characters --- rpg-docs/Model/Character/Spells.js | 95 +++++++++++++++---- .../views/character/inventory/inventory.js | 32 ++++--- .../client/views/character/spells/spells.js | 31 ++++-- 3 files changed, 117 insertions(+), 41 deletions(-) diff --git a/rpg-docs/Model/Character/Spells.js b/rpg-docs/Model/Character/Spells.js index ae85cf5f..2b5876b2 100644 --- a/rpg-docs/Model/Character/Spells.js +++ b/rpg-docs/Model/Character/Spells.js @@ -130,53 +130,98 @@ var checkMovePermission = function(spellId, parent, destinationOnly) { } }; -var moveSpell = function(spellId, parentCollection, parentId) { //moving spells between characters is NOT YET SUPPORTED :O +var moveSpell = function(spellId, targetCollection, targetId) { var spell = Spells.findOne(spellId); if (!spell) return; - parentCollection = parentCollection || spell.parent.collection; - parentId = parentId || spell.parent.id; + targetCollection = targetCollection || spell.parent.collection; + targetId = targetId || spell.parent.id; if (Meteor.isServer) { - checkMovePermission(spellId, {collection: parentCollection, id: parentId}, false); + checkMovePermission(spellId, {collection: targetCollection, id: targetId}, false); } - //update the spell provided the update will actually change something - if ( - spell.parent.collection !== parentCollection || - spell.parent.id !== parentId - ){ + if (targetCollection == "Characters") { //then we are copying the spell to a different character. + targetList = SpellLists.findOne({"charId": targetId}); + targetListId = targetList && targetList._id; + if (!targetListId) { + targetListId = SpellLists.insert({ //create a spell list if we don't already have one + name: "New SpellList", + charId: targetId, + saveDC: "8 + intelligenceMod + proficiencyBonus", + attackBonus: "intelligenceMod + proficiencyBonus", + }); + } + Spells.update( spellId, {$set: { - "parent.collection": parentCollection, - "parent.id": parentId, + charId: targetId, + "parent.collection": "SpellLists", + "parent.id": targetListId, }} ); } + else { //we are moving the spell within the same character + //update the spell provided the update will actually change something + if ( + spell.parent.collection !== targetCollection || + spell.parent.id !== targetId + ){ + Spells.update( + spellId, + {$set: { + "parent.collection": targetCollection, + "parent.id": targetId, + }} + ); + } + } }; -var copySpell = function(spellId, parentCollection, parentId) { +var copySpell = function(spellId, targetCollection, targetId) { var spell = Spells.findOne(spellId); if (!spell) return; - parentCollection = parentCollection || spell.parent.collection; - parentId = parentId || spell.parent.id; + targetCollection = targetCollection || spell.parent.collection; + targetId = targetId || spell.parent.id; if (Meteor.isServer) { - checkMovePermission(spellId, {collection: parentCollection, id: parentId}, true); //we're only reading from the source character + checkMovePermission(spellId, {collection: targetCollection, id: targetId}, true); //we're only reading from the source character } - if (spell.parentCollection == "Characters") { //then we are copying the spell to a different character. - //TODO: handle this - } else { //else we are copying the spell within the same character + if (targetCollection == "Characters") { //then we are copying the spell to a different character. + targetList = SpellLists.findOne({"charId": targetId}); + targetListId = targetList && targetList._id; + if (!targetListId) { + targetListId = SpellLists.insert({ //create a spell list if we don't already have one + name: "New SpellList", + charId: targetId, + saveDC: "8 + intelligenceMod + proficiencyBonus", + attackBonus: "intelligenceMod + proficiencyBonus", + }); + } + newSpell = _.clone(spell); delete newSpell._id; newSpellId = Spells.insert(newSpell); //add a new copy of the spell Spells.update( newSpellId, {$set: { - "parent.collection": parentCollection, - "parent.id": parentId, + charId: targetId, + "parent.collection": "SpellLists", + "parent.id": targetListId, + }} + ); + } + else { //else we are copying the spell within the same character + newSpell = _.clone(spell); + delete newSpell._id; + newSpellId = Spells.insert(newSpell); //add a new copy of the spell + Spells.update( + newSpellId, + {$set: { + "parent.collection": targetCollection, + "parent.id": targetId, }} ); } @@ -194,4 +239,14 @@ Meteor.methods({ check(spellListId, String); copySpell(spellId, "SpellLists", spellListId); }, + moveSpellToCharacter: function(spellId, charId) { + check(spellId, String); + check(charId, String); + moveSpell(spellId, "Characters", charId); + }, + copySpellToCharacter: function(spellId, charId) { + check(spellId, String); + check(charId, String); + copySpell(spellId, "Characters", charId); + }, }); \ No newline at end of file diff --git a/rpg-docs/client/views/character/inventory/inventory.js b/rpg-docs/client/views/character/inventory/inventory.js index 5a06a21f..bddd2014 100644 --- a/rpg-docs/client/views/character/inventory/inventory.js +++ b/rpg-docs/client/views/character/inventory/inventory.js @@ -334,21 +334,23 @@ Template.layout.events({ Session.set("inventory.dragItemId", null); }, "drop .characterRepresentative": function(event, instance) { - var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items"); - if (event.ctrlKey){ - //split the stack to the container - pushDialogStack({ - template: "splitStackDialog", - data: { - id: itemId, - parentCollection: "Characters", - parentId: this._id, - }, - }); - } else { - //move item to the character - Meteor.call("moveItemToCharacter", itemId, this._id); + if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){ + var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items"); + if (event.ctrlKey){ + //split the stack to the container + pushDialogStack({ + template: "splitStackDialog", + data: { + id: itemId, + parentCollection: "Characters", + parentId: this._id, + }, + }); + } else { + //move item to the character + Meteor.call("moveItemToCharacter", itemId, this._id); + } + Session.set("inventory.dragItemId", null); } - Session.set("inventory.dragItemId", null); }, }); diff --git a/rpg-docs/client/views/character/spells/spells.js b/rpg-docs/client/views/character/spells/spells.js index 7b5bf170..04a9ea63 100644 --- a/rpg-docs/client/views/character/spells/spells.js +++ b/rpg-docs/client/views/character/spells/spells.js @@ -337,26 +337,45 @@ Template.spells.events({ Template.layout.events({ "dragstart .spellItem": function(event, instance){ event.originalEvent.dataTransfer.setData("dicecloud-id/spells", this._id); - Session.set("inventory.dragSpellId", this._id); + Session.set("spellLists.dragSpellId", this._id); }, "dragend .spellItem": function(event, instance){ - Session.set("inventory.dragSpellId", null); + Session.set("spellLists.dragSpellId", null); }, - "dragover .spellList, dragenter .spellList": - function(event, instance){ + "dragover .spellList, dragenter .spellList": function(event, instance){ if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/spells")){ event.preventDefault(); } }, "drop .spellList": function(event, instance){ var spellId = event.originalEvent.dataTransfer.getData("dicecloud-id/spells"); - //move spell to new list if (event.ctrlKey){ + //copy spell to new list Meteor.call("copySpellToList", spellId, this._id); } else { + //move spell to new list Meteor.call("moveSpellToList", spellId, this._id); } - Session.set("inventory.dragSpellId", null); + Session.set("spellLists.dragSpellId", null); + }, + + "dragover .characterRepresentative, dragenter .characterRepresentative": function(event, instance){ + if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/spells")){ + event.preventDefault(); + } + }, + "drop .characterRepresentative": function(event, instance) { + if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/spells")){ //to prevent conflicts with item drag/drop + var spellId = event.originalEvent.dataTransfer.getData("dicecloud-id/spells"); + if (event.ctrlKey){ + //copy spell to character + Meteor.call("copySpellToCharacter", spellId, this._id); + } else { + //move spell to character + Meteor.call("moveSpellToCharacter", spellId, this._id); + } + Session.set("spellLists.dragSpellId", null); + } }, }); \ No newline at end of file From a1d9f7f5bb40b3511ce87365dcf053ad09ea622d Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 9 Aug 2017 02:18:46 +0100 Subject: [PATCH 4/7] Added ability to remove self from readers. Closes #125. --- .../unshareCharacterConfirmation.html | 23 ++++++++++++++ .../unshareCharacterConfirmation.js | 31 +++++++++++++++++++ .../views/character/characterSheet.html | 10 ++++++ .../client/views/character/characterSheet.js | 7 +++++ rpg-docs/lib/methods/removeMeFromReaders.js | 11 +++++++ 5 files changed, 82 insertions(+) create mode 100644 rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.html create mode 100644 rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.js create mode 100644 rpg-docs/lib/methods/removeMeFromReaders.js diff --git a/rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.html b/rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.html new file mode 100644 index 00000000..c5c689d8 --- /dev/null +++ b/rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.html @@ -0,0 +1,23 @@ + + diff --git a/rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.js b/rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.js new file mode 100644 index 00000000..de240ee3 --- /dev/null +++ b/rpg-docs/client/views/character/characterSettings/unshareCharacterConfirmation.js @@ -0,0 +1,31 @@ +Template.unshareCharacterConfirmation.onCreated(function() { + this.canUnshare = new ReactiveVar(false); +}); + +Template.unshareCharacterConfirmation.helpers({ + cantUnshare: function() { + return !Template.instance().canUnshare.get(); + }, + getStyle: function() { + if (Template.instance().canUnshare.get()) { + return "background: #d23f31; color: white;"; + } + } +}); + +Template.unshareCharacterConfirmation.events({ + "change #nameInput, input #nameInput": function(event, instance) { + var can = instance.find("#nameInput").value === this.name; + instance.canUnshare.set(can); + }, + "click #unshareButton": function(event, instance) { + if (instance.find("#nameInput").value === this.name) { + setTimeout(popDialogStack, 100); //weird things happen without the delay. + Router.go("/characterList"); + Meteor.call("removeMeFromReaders", this._id); + } + }, + "click .cancelButton": function(event, instance){ + popDialogStack(); + }, +}); diff --git a/rpg-docs/client/views/character/characterSheet.html b/rpg-docs/client/views/character/characterSheet.html index cc6f11e3..9fd7a65f 100644 --- a/rpg-docs/client/views/character/characterSheet.html +++ b/rpg-docs/client/views/character/characterSheet.html @@ -30,6 +30,16 @@ + {{else}} + + + + + + Unshare + + + {{/if}}
diff --git a/rpg-docs/client/views/character/characterSheet.js b/rpg-docs/client/views/character/characterSheet.js index c7052c44..528cf42d 100644 --- a/rpg-docs/client/views/character/characterSheet.js +++ b/rpg-docs/client/views/character/characterSheet.js @@ -210,4 +210,11 @@ Template.characterSheet.events({ element: event.currentTarget.parentElement.parentElement, }); }, + "click #unshareCharacter": function(event, instance){ + pushDialogStack({ + data: this, + template: "unshareCharacterConfirmation", + element: event.currentTarget.parentElement.parentElement, + }); + }, }); diff --git a/rpg-docs/lib/methods/removeMeFromReaders.js b/rpg-docs/lib/methods/removeMeFromReaders.js new file mode 100644 index 00000000..72e8fb63 --- /dev/null +++ b/rpg-docs/lib/methods/removeMeFromReaders.js @@ -0,0 +1,11 @@ +Meteor.methods({ + removeMeFromReaders: function(charId) { + var userId = Meteor.userId(); + var character = Characters.findOne(charId); + + if (!character) return; + if (!_.contains(character.readers, userId)) return; + + Characters.update(charId, {$pull: {"readers": userId}}); //we don't check write permission as you should always be able to remove youself from readers + } +}); \ No newline at end of file From 85baf4e5d14715dd56e289d25c13d7f6fd57d55e Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 4 Sep 2017 15:35:52 +0100 Subject: [PATCH 5/7] Added attacks to features (resolves #84) --- .../views/character/features/featureDialog/featureDialog.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rpg-docs/client/views/character/features/featureDialog/featureDialog.html b/rpg-docs/client/views/character/features/featureDialog/featureDialog.html index 504afd7a..11219e2b 100644 --- a/rpg-docs/client/views/character/features/featureDialog/featureDialog.html +++ b/rpg-docs/client/views/character/features/featureDialog/featureDialog.html @@ -36,6 +36,7 @@ {{> effectsViewList charId=charId parentId=_id}} {{> proficiencyViewList charId=charId parentId=_id}} + {{> attacksViewList charId=charId parentId=_id}} From 5f35c71c9de37acbf9afa00b43e44db27265ad43 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 4 Sep 2017 15:50:57 +0100 Subject: [PATCH 6/7] Added "carried" checkbox to container edit screen Resolves #85 --- .../character/inventory/containerDialog/containerDialog.html | 3 +++ .../character/inventory/containerDialog/containerDialog.js | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.html b/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.html index c6be3bcd..e11b1202 100644 --- a/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.html +++ b/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.html @@ -17,6 +17,9 @@ + + Carried +

diff --git a/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.js b/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.js index 2bce0102..288999c5 100644 --- a/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.js +++ b/rpg-docs/client/views/character/inventory/containerDialog/containerDialog.js @@ -54,4 +54,8 @@ Template.containerEdit.events({ trimStrings: false, }); }, + "change #carriedToggle": function(event, instance){ + var carried = !this.isCarried; + Containers.update(this._id, {$set: {isCarried: carried}}); + } }); From ad9ccbe7efcdb3b6d760a9cb71785116675f4e96 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 5 Sep 2017 14:31:26 +0100 Subject: [PATCH 7/7] Removed blank bit of card on features with no short description --- rpg-docs/client/views/character/features/features.css | 4 ++++ rpg-docs/client/views/character/features/features.html | 2 +- rpg-docs/client/views/character/features/features.js | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rpg-docs/client/views/character/features/features.css b/rpg-docs/client/views/character/features/features.css index 6d6d9e6c..520f9e55 100644 --- a/rpg-docs/client/views/character/features/features.css +++ b/rpg-docs/client/views/character/features/features.css @@ -6,6 +6,10 @@ margin-bottom: 8px; } +.card.featureCard .bottom { + padding-bottom: 8px; +} + .containerMain.featureDescription { white-space: pre-line; } diff --git a/rpg-docs/client/views/character/features/features.html b/rpg-docs/client/views/character/features/features.html index b3499845..ca7f95b9 100644 --- a/rpg-docs/client/views/character/features/features.html +++ b/rpg-docs/client/views/character/features/features.html @@ -78,7 +78,7 @@
{{/if}}
- {{#if description}} + {{#if hasCharacters (evaluateShortString charId description)}}
{{#markdown}}{{evaluateShortString charId description}}{{/markdown}}
diff --git a/rpg-docs/client/views/character/features/features.js b/rpg-docs/client/views/character/features/features.js index 2ab8f277..92a5eb06 100644 --- a/rpg-docs/client/views/character/features/features.js +++ b/rpg-docs/client/views/character/features/features.js @@ -56,6 +56,9 @@ Template.features.helpers({ var profs = Proficiencies.find({charId: this._id, type: "tool"}); return removeDuplicateProficiencies(profs); }, + hasCharacters: function(string){ + return string.match(/\S/); + }, }); Template.features.events({