A simple "keypress"
event handler that silently listens to what is typed outside of form fields
spell.js is a simple library that handles the capture of custom words typed in any point of the page. The library does not keep track of what users type, but only triggers events after a user has typed specific keywords. Its purpose is not spy users' actions or to use JavaScript to handle passwords or encrypted contents, but rather to enable custom commands that should not be publicly advertised.
For instance, imagine you have a website, and this possesses an administration panel protected by a password. On the one hand you might want to be able to access the panel easily, so a link to it in your home page would be helpful. On the other hand you might not want that the world sees a link to something no one can access except you. The solution would be therefore to hide the link somehow.
With this library you could easily solve this situation by generating, for example, a redirect to the administration panel when you type the words “it's me” anywhere on the page. In this way an attacker will still be able to see the location of the administration page by looking at the code – but that page is protected by a password (server-side), and for most platforms the location of the administration page is anyway known (think of Wordpress, for example). However you will have reached your goal of not advertising the location of the administration panel and still be able to reach it easily.
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="spell.js"></script>
<title>spell.js – Example</title>
<script type="text/javascript">
var mySpell = new Spell(/* place here your keyword...: */ "silence", function (element) {
alert("You have typed the word \"" + this.content + "\"!");
});
</script>
</head>
<body>
<p>Type the word “silence” in any point of the page...</p>
</body>
</html>
new Spell()
Spell.getStatus()
Spell.setStatus()
Spell.pronounce()
Spell.unspellAll()
Spell.makeSilence()
Spell.activeList()
Spell.prototype.enable()
Spell.prototype.disable()
Spell.prototype.unspell()
spell.content
spell.ontype
spell.reticent
spell.noticeable
spell.INDEX
(opaque / read-only)
"enchanted"
"cursed"
The Spell
constructor creates a "keypress"
event listener (attached to the window
object) able to capture the typing of a custom word/sentence in any point of the page and trigger an event.
new Spell([content[, callback[, reticent[, disabled]]]])
- content (optional): The string that triggers the event (default value:
null
) - callback (optional): The function that will be invoked when the
spell
is triggered (default value:null
) - reticent (optional): A boolean expressing whether the
spell
is active only on elements that possess the"enchanted"
class – see below (default value:false
) - disabled (optional): A boolean expressing whether the
spell
is active or not (default value:false
)
The spell
object created.
The parameters content
, callback
and reticent
can be assigned later by manually setting the properties spell.content
, spell.ontype
and spell.reticent
on the instance object (see below).
The parameter disabled
can be modified later by invoking the instance's methods spell.disable()
and spell.enable()
(see below).
If the parameter reticent
is set to false
or omitted, the handler will listen to what the user types anywhere except in form fields and editable contents. In order to enable the listening in form fields and editable contents it is necessary to assign the class "enchanted"
to them (see below).
This function should be invoked with the new
operator.
This method gets the listening status of the global Spell
object.
Spell.getStatus()
No parameters are required.
This method returns true
if the global Spell
object is currently listening, false
otherwise.
This method sets the listening status of the global Spell
object.
Spell.setStatus(active)
- active: A boolean expressing whether the global
Spell
object must be set on listening mode or not
This method returns false
if the spell
was already in the same listening status expressed by the active
parameter, true
otherwise.
This method checks whether a string matches one or more active keys. If it does, the typing event(s) will be triggered.
Spell.pronounce(content[, element])
- content: The string to check
- element (optional): The element to be passed to the function referenced by
spell.ontype
(default value:window
)
This method does not return anything.
Many devices, such as mobiles and tablets, do not possess a physical keyboard, but rather a virtual one. Normally the latter is not available outside of editable contents, hence in some cases it might be impossible for the user to be able to type anything at all in the page. To ensure the capability to silently type a word it might be necessary therefore, in some cases, to explore other ways.
The Spell.pronounce()
method allows to test a keyword via code, rather than via user input. By doing so it allows to build alternative ways to make certain that the user is able to type a keyword at least somewhere. See, for instance, the following example.
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="spell.js"></script>
<title>Spell.pronounce()</title>
</head>
<body>
<p>Type “foo” or “bar” in any point of the page...</p>
<p><strong>Note:</strong> If you use a virtual keyboard, <span id="five_times" style="cursor: default; text-decoration: underline; color: #0000ff;">click here <strong>five</strong> times</span> then type either “foo” or “bar”.</p>
<script type="text/javascript">
(function () {
function onKeywordMatches (oElement) {
alert("You have typed the word \"" + this.content + "\"!");
}
var
clickCounter = 0, myClickable = document.getElementById("five_times"),
mySpell1 = new Spell("foo", onKeywordMatches),
mySpell2 = new Spell("bar", onKeywordMatches);
document.addEventListener("click", function (oEvent) {
var bWrongPoint = true;
for (var oIter = oEvent.target; bWrongPoint && oIter; bWrongPoint = oIter !== myClickable, oIter = oIter.parentNode);
if (bWrongPoint) {
clickCounter = 0;
} else if (clickCounter < 4) {
clickCounter++;
oEvent.preventDefault();
} else {
clickCounter = 0;
Spell.pronounce(prompt("Tell me something..."), this);
oEvent.preventDefault();
}
}, false);
})();
</script>
</body>
</html>
This method resets all spell
s currently active (all matching characters typed so far will be forgotten).
Spell.unspellAll()
No parameters are required.
This method does not return anything.
This method disables all spell
s currently active.
Spell.makeSilence()
No parameters are required.
This method does not return anything.
This method gets the list of all spell
s currently active.
Spell.activeList()
No parameters are required.
This method returns a new array containing all spell
s currently active.
This method enables a spell
.
spell.enable()
No parameters are required.
This method returns false
if the spell
had been already enabled, true
otherwise.
This method disables a spell
.
spell.disable()
No parameters are required.
This method returns false
if the spell
had been already enabled, true
otherwise.
This method resets a spell
(all matching characters typed so far will be forgotten).
spell.unspell()
No parameters are required.
This method does not return anything.
This property defines the keyword that triggers the event.
console.log(spell.content);
spell.content = "hello world";
spell.unspell();
The content
property of a spell
can be edited after the spell
has been already created. In this case, however, it might be wished to reset the latter by invoking the method spell.unspell()
as well, as in the example above, or the new keyword will take advantage of the positive status of the previous keyword if some correct characters had been already typed.
This property contains a reference to the function that will be invoked when the typing event is triggered. The element
on which the final "keypress"
event has occurred will be passed as only argument, while the this
object will be the spell
itself. The function's return value will be ignored.
spell.ontype = function (element) {
/* [...] */
};
If set to true
, the spell
will be active only on elements that possess the "enchanted"
class (see below). Further screenings on the target elements can be performed using the element
parameter passed to the callback function (see spell.ontype
).
spell.reticent = true;
spell.reticent = false;
For further readings, see below (§ DOM).
If set to true
, spell.noticeable
prevents "keypress"
event's default actions whenever a typed character matches a spell
's content
property. Its default value is true
.
spell.noticeable = false;
spell.noticeable = true;
It is possible to see the effect of this property by assigning the "enchanted"
class to a text input and trying to type something inside it, first with spell.noticeable
set to true
, then to false
.
A value expressing the number of correct characters consecutively typed in respect to the content
property. The initial value is 0
.
This property is for internal purposes and should be considered opaque / read-only. To reset its value to 0
use the spell.unspell()
method (see above).
An element whose classList
contains the "enchanted"
token will be ensured to be a listener of typing events unless its classList
contains the "cursed"
token as well (see below).
<p><input type="text" class="enchanted" /></p>
<div contenteditable class="enchanted" style="height: 200px; width: 300px; border: 1px #000000 solid;"></div>
The spell
s whose reticent
property is set to false
will have every element of the page as listeners except the following ones:
HTMLInputElement
HTMLSelectElement
HTMLTextAreaElement
HTMLButtonElement
- Any
HTMLElement
whoseisContentEditable
property equalstrue
From this it follows that the "enchanted"
class is mandatory for the elements listed above in order to be considered listeners.
As for the spell
s whose reticent
property is set to true
instead, no HTML element will be considered listener unless it or its parent(s) possess the "enchanted"
class.
Note that the behavior determined by this class will be inherited by the child nodes (except when they are form fields and editable contents) – unless reverted by a "cursed"
class in the child.
This class is defined by the private variable sListenClass
in the code.
An element whose classList
contains the "cursed"
token will never be allowed to be a listener of typing events.
<div class="cursed" tabindex="15">Hello world</div>
<p><a href="http://www.example.com/" class="cursed">www.example.com</a></p>
This class will work only on elements that can receive focus()
. It is important to point out that, despite the focus()
method is defined on the HTMLElement
super-class, the only elements that can actually receive focus are:
HTMLAnchorElement
/HTMLAreaElement
with anhref
attributeHTMLInputElement
/HTMLSelectElement
/HTMLTextAreaElement
/HTMLButtonElement
without thedisabled
attributeHTMLIFrameElement
- Any element with a
tabindex
attribute >-1
If, for example, the <body>
element is listening to the typing events and a "cursed"
class has been assigned to its <p>
child, it will not be possible to prevent that the typing event will be captured even if the user has clicked on the <p>
element. This is so because a <p>
element cannot receive focus()
, and therefore can never appear as the actual listener of the event (but instead <body>
will).
In order for the <p>
element to receive focus()
and be an actual listener of typing events this must possess a tabindex
attribute > -1
. In this case it can make sense to assign the "cursed"
class to it to prevent its capturing of the typing.
Note that the behavior determined by this class will be inherited by the child nodes – unless reverted by a "enchanted"
class in the child.
This class is defined by the private variable sForbidClass
in the code.
The content
property of a spell
may contain any characters and symbols, but consider that special characters might not be present in some keyboards. The comparison with what the user types is always case-sensitive. Control keys are not captured.
The "cursed"
class has a higher priority in respect to the "enchanted"
class, and for elements possessing both classes only the former will be considered. Direct assignment however always prevails over inheritance, and a close inheritance prevails over a far one.
var mySpell = new Spell("myword", function (element) {
this.disable();
/* DO SOMETHING */
});
Imagine you want to highlight via CSS the parts of the page that will listen to the typing events. As for the spell.reticent === false
cases, the CSS selector will correspond to:
:not(.enchanted):not(.cursed):not(textarea):not(input):not(select):not(button):not([contenteditable=""]):not([contenteditable="true"]):not([contenteditable="TRUE"]),
.enchanted:not(.cursed) {
background-color: yellow !important;
}
As for the spell.reticent === true
cases, the CSS selector will correspond to:
.enchanted:not(.cursed) {
background-color: yellow !important;
}