--- /dev/null
+/**
+ * @file
+ * A Backbone Model for the state of a contextual link's trigger, list & region.
+ */
+
+(function (Drupal, Backbone) {
+ /**
+ * Models the state of a contextual link's trigger, list & region.
+ *
+ * @constructor
+ *
+ * @augments Backbone.Model
+ */
+ Drupal.contextual.StateModel = Backbone.Model.extend(/** @lends Drupal.contextual.StateModel# */{
+
+ /**
+ * @type {object}
+ *
+ * @prop {string} title
+ * @prop {bool} regionIsHovered
+ * @prop {bool} hasFocus
+ * @prop {bool} isOpen
+ * @prop {bool} isLocked
+ */
+ defaults: /** @lends Drupal.contextual.StateModel# */{
+
+ /**
+ * The title of the entity to which these contextual links apply.
+ *
+ * @type {string}
+ */
+ title: '',
+
+ /**
+ * Represents if the contextual region is being hovered.
+ *
+ * @type {bool}
+ */
+ regionIsHovered: false,
+
+ /**
+ * Represents if the contextual trigger or options have focus.
+ *
+ * @type {bool}
+ */
+ hasFocus: false,
+
+ /**
+ * Represents if the contextual options for an entity are available to
+ * be selected (i.e. whether the list of options is visible).
+ *
+ * @type {bool}
+ */
+ isOpen: false,
+
+ /**
+ * When the model is locked, the trigger remains active.
+ *
+ * @type {bool}
+ */
+ isLocked: false,
+ },
+
+ /**
+ * Opens or closes the contextual link.
+ *
+ * If it is opened, then also give focus.
+ *
+ * @return {Drupal.contextual.StateModel}
+ * The current contextual state model.
+ */
+ toggleOpen() {
+ const newIsOpen = !this.get('isOpen');
+ this.set('isOpen', newIsOpen);
+ if (newIsOpen) {
+ this.focus();
+ }
+ return this;
+ },
+
+ /**
+ * Closes this contextual link.
+ *
+ * Does not call blur() because we want to allow a contextual link to have
+ * focus, yet be closed for example when hovering.
+ *
+ * @return {Drupal.contextual.StateModel}
+ * The current contextual state model.
+ */
+ close() {
+ this.set('isOpen', false);
+ return this;
+ },
+
+ /**
+ * Gives focus to this contextual link.
+ *
+ * Also closes + removes focus from every other contextual link.
+ *
+ * @return {Drupal.contextual.StateModel}
+ * The current contextual state model.
+ */
+ focus() {
+ this.set('hasFocus', true);
+ const cid = this.cid;
+ this.collection.each((model) => {
+ if (model.cid !== cid) {
+ model.close().blur();
+ }
+ });
+ return this;
+ },
+
+ /**
+ * Removes focus from this contextual link, unless it is open.
+ *
+ * @return {Drupal.contextual.StateModel}
+ * The current contextual state model.
+ */
+ blur() {
+ if (!this.get('isOpen')) {
+ this.set('hasFocus', false);
+ }
+ return this;
+ },
+
+ });
+}(Drupal, Backbone));