diff --git a/media/target.svg b/media/target.svg
new file mode 100644
index 0000000..5715fe4
--- /dev/null
+++ b/media/target.svg
@@ -0,0 +1 @@
+
diff --git a/src/controllers/add-component-controller.js b/src/controllers/add-component-controller.js
index 723bd0a..b0a4cf8 100644
--- a/src/controllers/add-component-controller.js
+++ b/src/controllers/add-component-controller.js
@@ -13,13 +13,16 @@ var dom = require('@nymag/dom'),
*
* @param {Object} spaceParent
* @param {Function} callback
+ * @param {Boolean} invisible
*/
-function AddComponent(spaceParent, callback) {
+function AddComponent(spaceParent, callback, invisible) {
this.parent = spaceParent;
this.parentRef = spaceParent.getAttribute('data-uri');
- this.callback = callback;
+ this.callback = callback ? callback : _.noop;
+
+ this.invisible = invisible;
this.launchPane();
}
@@ -93,9 +96,9 @@ proto.makeNewComponentActive = function (targetEl) {
statusService.removeEditing(logic);
});
- statusService.setEditing(targetEl);
+ this.invisible ? _.noop() : statusService.setEditing(targetEl);
};
-module.exports = function (spaceParent, callback) {
- return new AddComponent(spaceParent, callback);
+module.exports = function (spaceParent, callback, invisible = false) {
+ return new AddComponent(spaceParent, callback, invisible);
};
diff --git a/src/controllers/space-settings-controller.js b/src/controllers/space-settings-controller.js
index 23905a3..2bbf630 100644
--- a/src/controllers/space-settings-controller.js
+++ b/src/controllers/space-settings-controller.js
@@ -10,18 +10,32 @@ var dom = require('@nymag/dom'),
proto = BrowseController.prototype;
/**
+ * Launch the controller to browse a Space.
+ *
* @param {Element} el
- * @param {Object} callbacks
+ * @param {Object} callbacks
+ * @param {Boolean} invisible
*/
-function BrowseController(el, callbacks) {
+function BrowseController(el, callbacks, invisible) {
/**
- * The Clay Space component data
- * @type {Object}
+ * The Space element
+ *
+ * @type {Element}
*/
this.el = el;
- this.spaceRef = el.getAttribute('data-uri');
+ /**
+ * The ref to the Space element
+ *
+ * @type {String}
+ */
+ this.spaceRef = el.getAttribute(references.referenceAttribute);
+ /**
+ * An object of callback functions
+ *
+ * @type {Object}
+ */
this.callbacks = callbacks;
/**
@@ -37,6 +51,13 @@ function BrowseController(el, callbacks) {
*/
this.componentList = [];
+ /**
+ * Boolean flag for whether or not you're launching
+ * a browse pane for an invisble Space
+ *
+ * @type {Boolean}
+ */
+ this.invisible = invisible;
this.findChildrenMakeList(this.el);
// Launch the pane
@@ -44,11 +65,11 @@ function BrowseController(el, callbacks) {
}
/**
- * Open the settins pane
+ * Open the settings pane
*/
proto.launchPane = function () {
var paneContent = this.markActiveInList(utils.createFilterableList(this.componentList, {
- click: this.listItemClick.bind(this),
+ click: this.invisible ? _.noop : this.listItemClick.bind(this),
reorder: this.reorder.bind(this),
settings: this.settings.bind(this),
remove: this.remove.bind(this),
@@ -56,14 +77,77 @@ proto.launchPane = function () {
addTitle: 'Add Component To Space'
}));
+ if (this.invisible) {
+ this.addInTarget(paneContent);
+ } else {
+ this.swapInTargetIcon(paneContent);
+ }
+
references.pane.open([{ header: this.el.getAttribute(references.dataPaneTitle) || 'Browse Space', content: paneContent }]);
};
+/**
+ * Add in a separate target button for invisible lists.
+ * This is important because the settings still need
+ * to point to the original component.
+ *
+ * @param {Element} content
+ */
+proto.addInTarget = function (content) {
+ var settingsIcons = dom.findAll(content, '.filtered-item-settings'),
+ targetSpaceBtn = references.tpl.get('.target-space');
+
+ // Whenever you append a document fragment like `targetSpaceBtn` into the DOM
+ // you are taking the content out of the document fragment. If you use `appendChild`
+ // then the HTML of the fragment is returned, but since we want to insert this
+ // icon before another icon we don't get that benefit. For this reason we have
+ // to clone the node (`cloneNode`) so that we can iterate through mutliple and
+ // items and apply the button.
+ _.forEach(settingsIcons, (icon) => {
+ var targetBtn,
+ targetSpaceBtnClone = targetSpaceBtn.cloneNode(true);
+
+ // Add in button
+ dom.insertBefore(icon, targetSpaceBtnClone);
+ // Find the button
+ targetBtn = dom.find(icon.parentElement, '.space-target');
+ // Attach event listeners
+ targetBtn.addEventListener('click', this.targetBtnClick.bind(this));
+ });
+};
+
+/**
+ * Event handler for the target button click event
+ *
+ * @param {Object} e
+ */
+proto.targetBtnClick = function (e) {
+ var componentUri = dom.closest(e.target, '[data-item-id]').getAttribute('data-item-id'),
+ component = dom.find(`[data-uri="${componentUri}"]`);
+
+ this.settings(component.parentElement.getAttribute(references.referenceAttribute));
+};
+
+/**
+ * Replace the settings icon with the target icon because
+ * accurate iconography is important
+ *
+ * @param {Element} content
+ */
+proto.swapInTargetIcon = function (content) {
+ var settingsBtnSvgs = dom.findAll(content, '.filtered-item-settings svg'),
+ targetSvg = references.tpl.get('.target-space-svg');
+
+ _.forEach(settingsBtnSvgs, function (icon) {
+ dom.replaceElement(icon, targetSvg.cloneNode(true));
+ });
+};
+
/**
* Launch the AddController
*/
proto.addComponent = function () {
- AddController(this.el, this.callbacks.add);
+ AddController(this.el, this.callbacks.add, this.invisible);
};
/**
@@ -75,28 +159,49 @@ proto.findChildrenMakeList = function (el) {
this.componentList = this.makeList(this.childComponents);
};
+/**
+ * Determine if it's the Logic or the child that should be
+ * used based on whether or not the Space is invisible
+ *
+ * @param {Element} logic
+ * @return {Element}
+ */
+proto.logicOrEmbedded = function (logic) {
+ return this.invisible ? dom.find(logic, '[data-uri]') : logic;
+};
+
/**
* Apply active styling to the proper item in the filterable list
+ *
* @param {element} listHtml
* @returns {element}
*/
proto.markActiveInList = function (listHtml) {
- _.each(this.childComponents, function (logicComponent) {
- if (statusService.isEditing(logicComponent)) {
- dom.find(listHtml, '[data-item-id="' + logicComponent.getAttribute('data-uri') + '"]').classList.add('active');
- }
- });
+ if (!this.invisible) { // An 'active' item isn't a thing when you can't see it...right?
+ _.each(this.childComponents, (logicComponent) => {
+ var targetUri = this.logicOrEmbedded(logicComponent).getAttribute('data-uri');
+ if (statusService.isEditing(logicComponent)) {
+ dom.find(listHtml, `[data-item-id="${targetUri}"]`).classList.add('active');
+ }
+ });
+ }
return listHtml;
};
+proto.logicFromChildRef = function (ref) {
+ return dom.closest(dom.find(`[data-uri="${ref}"]`), '[data-logic]');
+};
+
/**
* Delete a component from a space
*
* @param {string} id The `id` value of the item in the filterable list that was clicked
*/
proto.remove = function (id) {
- removeService.removeLogic(id, this.el)
+ var logicUri = this.invisible ? this.logicFromChildRef(id).getAttribute('data-uri') : id;
+
+ removeService.removeLogic(logicUri, this.el)
.then(() => {
// Make new component list from the returned HTML
this.findChildrenMakeList(this.el);
@@ -116,17 +221,19 @@ proto.remove = function (id) {
* @return {Object}
*/
proto.makeList = function (components) {
- return _.map(components, function (item) {
- var childComponent = dom.find(item, '[data-uri]'),
- componentType = references.getComponentNameFromReference(childComponent.getAttribute('data-uri')),
- componentTitle = references.label(componentType),
- readouts = logicReadoutService(item);
+ return _.map(components, (item) => {
+ var childComponentUri = dom.find(item, '[data-uri]').getAttribute('data-uri'), // Get the child of the logic
+ componentType = references.getComponentNameFromReference(childComponentUri), // Get it's name
+ componentTitle = references.label(componentType), // Make the title
+ readouts = logicReadoutService(item), // Get logic readouts
+ returnId = this.invisible ? childComponentUri : item.getAttribute('data-uri');
- componentTitle = readouts ? componentTitle + readouts : componentTitle;
+ componentTitle = readouts ? componentTitle + readouts : componentTitle; // Great the title
+ // Return an object representing the Logic and it's child
return {
title: componentTitle,
- id: item.getAttribute('data-uri')
+ id: returnId
};
});
};
@@ -138,9 +245,18 @@ proto.makeList = function (components) {
* @param {number} oldIndex
*/
proto.reorder = function (id, newIndex, oldIndex) {
+
var data = { _ref: this.spaceRef },
- content = _.map(this.componentList, function (item) {
- return { _ref: item.id };
+ content = _.map(this.componentList, (item) => {
+ var id = item.id;
+
+ // If invisible then `item.id` will be the direct component's
+ // uri and not its parent Logic's. Need to fix that.
+ if (this.invisible) {
+ id = this.logicFromChildRef(id).getAttribute('data-uri');
+ }
+
+ return { _ref: id };
});
content.splice(newIndex, 0, content.splice(oldIndex, 1)[0]); // reorder the array
@@ -230,12 +346,13 @@ proto.settings = function (id) {
/**
* Make a new Space Settings instance
*
- * @param {Object} parent
- * @param {Object} callbacks
+ * @param {Object} el
+ * @param {Object} callbacks
+ * @param {Boolean} invisible
* @return {BrowseController}
*/
-function spaceSettings(parent, callbacks) {
- return new BrowseController(parent, callbacks);
+function spaceSettings(el, callbacks = {}, invisible = false) {
+ return new BrowseController(el, callbacks, invisible);
};
module.exports = spaceSettings;
diff --git a/src/index.js b/src/index.js
index 9aecabe..3e55a02 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,24 +1,33 @@
var selector = require('./services/selector'),
SpaceController = require('./controllers/space-controller'),
+ invisibleLists = require('./services/invisible-lists'),
utils = require('./services/utils');
// Load styles
require('./styleguide/styles.scss');
function updateSelector(el, options, parent) {
- var isSpaceComponent = utils.checkIfSpace(options.ref),
- availableSpaces = utils.spaceInComponentList(parent);
+ var isSpaceComponent, availableSpaces;
+
+ if (utils.checkIfSpaceEdit(options.ref)) {
+ return;
+ }
+
+ isSpaceComponent = utils.checkIfSpace(options.ref);
+ availableSpaces = utils.spaceInComponentList(parent);
if (availableSpaces && availableSpaces.length > 0 && !isSpaceComponent) {
selector.addAvailableSpaces(el, availableSpaces);
selector.addCreateSpaceButton(el, options, parent);
selector.stripSpaceFromComponentList(el);
+
}
if (isSpaceComponent) {
selector.addAvailableSpaces(el, availableSpaces);
SpaceController(el, parent);
selector.addToComponentList(el, options, parent);
+
}
}
@@ -26,4 +35,5 @@ window.kiln = window.kiln || {};
window.kiln.plugins = window.kiln.plugins || {};
window.kiln.plugins['spaces-edit'] = function initSpaces() {
window.kiln.on('add-selector', updateSelector);
+ window.kiln.on('component-pane:create-invisible-tab', invisibleLists);
};
diff --git a/src/services/invisible-lists.js b/src/services/invisible-lists.js
new file mode 100644
index 0000000..7c7db7c
--- /dev/null
+++ b/src/services/invisible-lists.js
@@ -0,0 +1,154 @@
+var dom = require('@nymag/dom'),
+ _ = require('lodash'),
+ createService = require('./create-service'),
+ removeService = require('./remove-service'),
+ references = require('references'),
+ utils = require('./utils'),
+ SpaceSettings = require('../controllers/space-settings-controller'),
+ settingsCallbacks = {
+ remove: removeCallback
+ };
+
+/**
+ * Modify the HTML of an invisible component list
+ * tab to include anything related to Spaces
+ *
+ * @param {Element} invisibleList
+ */
+function openInvisibleList(invisibleList) {
+ var spaceableElements = dom.findAll(invisibleList.list.el, `[${references.dataAvailableSpaces}]`),
+ spaceableRefs = _.compact(_.map(spaceableElements, function (element) {
+ var uri = element.getAttribute(references.referenceAttribute);
+
+ return _.includes(uri, references.spacePrefix) ? null : uri;
+ })),
+ listItemElements = dom.findAll(invisibleList.content, '[data-item-id]'),
+ spaceComponents = _.filter(listItemElements, function (el) {
+ var itemId = el.getAttribute('data-item-id');
+
+ return _.includes(itemId, references.spacePrefix) && !_.includes(itemId, references.spaceEdit);
+ });
+
+ _.forEach(filterListItems(spaceableRefs, listItemElements), addCreateButton.bind(null, invisibleList));
+ _.forEach(spaceComponents, addBrowseButton);
+}
+
+/**
+ * Find all the list items in the filterable list
+ * that can be made into a Space
+ *
+ * @param {Array} targetRefs
+ * @param {Element} items
+ * @return {Array}
+ */
+function filterListItems(targetRefs, items) {
+ return _.filter(items, function (item) {
+ return _.includes(targetRefs, item.getAttribute('data-item-id'));
+ });
+}
+
+/**
+ * Add in the 'Create Space' button
+ * @param {Object} invisibleList
+ * @param {Element} item
+ */
+function addCreateButton(invisibleList, item) {
+ var settings = dom.find(item, '.filtered-item-settings'),
+ createSpaceButton = references.tpl.get('.create-space-list'),
+ createButton;
+
+ dom.insertBefore(settings, createSpaceButton);
+
+ createButton = dom.find(item, '.space-create');
+ createButton.addEventListener('click', createSpaceFromInvisibleComponent.bind(null, item, invisibleList));
+}
+
+/**
+ * Create a Space out of the invisible component
+ *
+ * @param {Element} item
+ * @param {Object} invisibleList
+ */
+function createSpaceFromInvisibleComponent(item, invisibleList) {
+ var uri = item.getAttribute('data-item-id'),
+ element = dom.find(document, `[data-uri="${uri}"]`),
+ parent = dom.closest(element.parentElement, '[data-uri]');
+
+ createService.createSpace({ ref: uri, data: { _ref: uri } }, {
+ el: parent,
+ ref: invisibleList.layoutRef,
+ path: invisibleList.list.path
+ });
+}
+
+/**
+ * Add in the proper browse button
+ *
+ * @param {Element} item
+ */
+function addBrowseButton(item) {
+ var settings = dom.find(item, '.filtered-item-settings'),
+ remove = dom.find(item, '.filtered-item-remove'),
+ spaceUri = item.getAttribute('data-item-id'),
+ spaceElement = dom.find(document, `[data-uri="${spaceUri}"]`),
+ browseSpaceButton = references.tpl.get('.browse-space-list'),
+ browseButton;
+
+ dom.insertBefore(settings, browseSpaceButton);
+ settings.classList.add('kiln-hide');
+ remove.classList.add('kiln-hide');
+ // Find the browse button
+ browseButton = dom.find(item, '.space-browse');
+ // Add count of elements
+ getBrowseCount(spaceElement, browseButton);
+ // Create the browse pane
+ browseButton.addEventListener('click', function () {
+ launchBrowse(spaceElement);
+ });
+};
+
+/**
+ * Open the browse pane for a Space
+ *
+ * @param {Element} spaceElement
+ */
+function launchBrowse(spaceElement) {
+ SpaceSettings(spaceElement, settingsCallbacks, true);
+}
+
+/**
+ * The callback function when a component is removed.
+ * Either remove the Logic and re-open the pane or
+ * remove the whole Space
+ *
+ * @param {Element} space
+ */
+function removeCallback(space) {
+ var logics = utils.findAllLogic(space),
+ parentPath;
+
+ if (logics.length) {
+ launchBrowse(space);
+ } else {
+ parentPath = dom.closest(space, '[data-editable]').getAttribute('data-editable');
+ references.edit.getLayout()
+ .then(function (data) {
+ references.pane.close();
+ return removeService.removeSpace(space, { ref: data, path: parentPath });
+ });
+ }
+}
+
+/**
+ * Add the count to the browse button
+ *
+ * @param {Element} spaceElement
+ * @param {Element} button
+ */
+function getBrowseCount(spaceElement, button) {
+ var count = dom.find(button, '.logic-count');
+
+ count.innerHTML = dom.findAll(spaceElement, '[data-logic]').length;
+}
+
+module.exports = openInvisibleList;
diff --git a/src/services/references.js b/src/services/references.js
index 9c318bf..5483953 100644
--- a/src/services/references.js
+++ b/src/services/references.js
@@ -9,7 +9,6 @@ kilnServices = window.kiln.services;
referencesObj = _.assign({
spaceEdit: 'clay-space-edit',
spacePrefix: 'clay-space',
- spaceClass: 'clay-space',
dataAvailableSpaces: 'data-spaces-available',
dataPaneTitle: 'data-space-browse-title',
render: kilnServices.render,
diff --git a/src/services/selector.js b/src/services/selector.js
index b0a97da..ca548d2 100644
--- a/src/services/selector.js
+++ b/src/services/selector.js
@@ -121,7 +121,7 @@ function addBrowseButton(logicComponent) {
* @param {Object} callbacks [description]
*/
function launchBrowsePane(spaceElement, callbacks) {
- SpaceSettings(spaceElement, callbacks);
+ SpaceSettings(spaceElement, callbacks, false);
}
function addRemoveButton(logic) {
@@ -142,7 +142,7 @@ function addRemoveButton(logic) {
function stripSpaceFromComponentList(el) {
var addButton = dom.find(el, '.selected-add'),
components = addButton.getAttribute('data-components').split(','),
- componentsSansSpace = _.pull(components, references.spaceClass);
+ componentsSansSpace = _.pull(components, references.spacePrefix);
addButton.setAttribute('data-components', componentsSansSpace);
}
diff --git a/src/services/utils.js b/src/services/utils.js
index e9a50f8..3041f42 100644
--- a/src/services/utils.js
+++ b/src/services/utils.js
@@ -70,6 +70,16 @@ function checkIfSpace(ref) {
return _.includes(ref, references.spacePrefix);
}
+/**
+ * Checks if a reference is the Space Edit component.
+ *
+ * @param {String} ref The uri of a component
+ * @return {Boolean}
+ */
+function checkIfSpaceEdit(ref) {
+ return _.includes(ref, references.spaceEdit);
+}
+
/**
* Return an array of all components with the
* `data-logic` attribute
@@ -86,4 +96,5 @@ module.exports.spaceInComponentList = spaceInComponentList;
module.exports.availableSpaces = availableSpaces;
module.exports.createFilterableList = createFilterableList;
module.exports.checkIfSpace = checkIfSpace;
+module.exports.checkIfSpaceEdit = checkIfSpaceEdit;
module.exports.findAllLogic = findAllLogic;
diff --git a/src/styleguide/styles.scss b/src/styleguide/styles.scss
index 3f12c2c..fcb267c 100644
--- a/src/styleguide/styles.scss
+++ b/src/styleguide/styles.scss
@@ -20,19 +20,38 @@ $UI_BUTTON: #694C79;
border-left: 1px solid $UI_PRIMARY;
}
+.logic-count {
+ position: absolute;
+}
+
.space-browse {
border-left: 1px solid $UI_PRIMARY;
position: relative;
.logic-count {
bottom: 13px;
- position: absolute;
right: 7px;
}
}
// Readouts inside the logic browse pane
+.filtered-list-button {
+ background: none;
+ border: 0;
+ cursor: pointer;
+ flex: 0 0 auto;
+ margin-left: 10px;
+ outline: none;
+ padding: 0;
+ position: relative;
+
+ .logic-count {
+ bottom: -5px;
+ right: -8px;
+ }
+}
+
.filtered-item-title-logic-readout {
display: flex;
flex-wrap: wrap;
@@ -46,9 +65,9 @@ $UI_BUTTON: #694C79;
margin-right: 8px;
& svg {
- width: 12px;
height: 12px;
vertical-align: text-top;
+ width: 12px;
}
& > .readout-icon {
diff --git a/template.nunjucks b/template.nunjucks
index 4b37569..b88e0d5 100644
--- a/template.nunjucks
+++ b/template.nunjucks
@@ -27,6 +27,33 @@
+ {# Filter List Creat Space Button #}
+
+
+
+
+ {# Filter List Browse Button #}
+
+
+
+
+ {# Target Space Button #}
+
+
+
+
+ {# Target SVG #}
+
+ {% include 'public/media/components/clay-space-edit/target.svg' %}
+
+
{# Readout Icons #}
{# Need more icons? Submit a PR to https://github.com/nymag/clay-space-edit with the icon #}