describe('caret', () => { let textNode = document.createTextNode('Hello World'), span = create(`<span ${editAttr}="foo"></span>`), el = create(`<div ${refAttr}="domain.com/_components/hi"></div>`); span.appendChild(textNode); el.appendChild(span); describe('getClickOffset', () => { const fn = lib.getClickOffset; test('gets the offset of clicked text', () => { let doc = { caretPositionFromPoint: jest.fn(), createElement: document.createElement.bind(document) }; doc.caretPositionFromPoint.mockReturnValue({ offsetNode: textNode, offset: 5 }); components.getData.mockReturnValue('Hello World'); expect(fn({ clientX: 0, clientY: 0 }, doc)).toBe(5); }); test('gets the offset when clicking past text', () => { let doc = { caretRangeFromPoint: jest.fn(), createElement: document.createElement.bind(document) }; doc.caretRangeFromPoint.mockReturnValue({ startContainer: span, startOffset: 5 }); components.getData.mockReturnValue('Hello World'); span.classList.add(selectorClass); expect(fn({ clientX: 0, clientY: 0 }, doc)).toBe(11); }); test('returns 0 for placeholders', () => { let doc = { caretRangeFromPoint: jest.fn(), createElement: document.createElement.bind(document) }; doc.caretRangeFromPoint.mockReturnValue({ startContainer: span, startOffset: 5 }); components.getData.mockReturnValue('Hello World'); span.classList.add(placeholderClass); expect(fn({ clientX: 0, clientY: 0 }, doc)).toBe(0); }); test('returns 0 if data is not in parent element', () => { let doc = { caretPositionFromPoint: jest.fn(), createElement: document.createElement.bind(document) }; doc.caretPositionFromPoint.mockReturnValue({ offsetNode: textNode, offset: 5 }); components.getData.mockReturnValue('Other Text'); span.classList.remove(placeholderClass); expect(fn({ clientX: 0, clientY: 0 }, doc)).toBe(0); }); }); });
it('replaces the result.el', function () { // We could call this a "root" behavior var result, elBefore = dom.create('<div class="el-before"></div>'); fixture.el = elBefore; result = lib(fixture, args); expect(result.el.querySelector('.el-before')).to.eql.null; });
module.exports = function (result) { var name = result.name; result.bindings.data.value = dom.uri(); // returns the current uri (for now) // todo: if behaviors supported promises, we could get the full page id result.el.appendChild(dom.create(`<input type="hidden" class="input-text" rv-field="${name}" rv-value="${name}.data.value" />`)); return result; };
function createOverlayEl(innerEl) { var el = dom.create(` <div class="editor-overlay-background"> <div class="editor-overlay"></div> </div> `); dom.find(el, '.editor-overlay').appendChild(innerEl); return el; }
it('allows access to component data and sorted list of components', function () { var spy = sandbox.spy(), ref = '/components/a', data = {}, rules = [{ validate: spy }]; dom.findAll.withArgs('[data-uri]').returns([dom.create('<section data-uri="' + ref + '" />')]); edit.getData.withArgs(ref).returns(Promise.resolve(data)); return fn(rules).then(function () { expect(spy.args[0][0]).to.deep.equal({refs: {'/components/a': {}}, components: ['a']}); }); });
function createInlineFormEl(innerEl) { var el = dom.create(` <section class="editor editor-inline"> <form> <div class="input-container"></div> <button type="submit" class="hidden-submit"></button> </form> </section> `); dom.find(el, '.input-container').appendChild(innerEl); return el; }
module.exports = function (result, args) { var name = result.name, label = args.label, tpl = ` <label class="input-label"> <input class="input-checkbox" rv-field="${name}" type="checkbox" rv-checked="${name}.data.value" /><span class="checkbox-label">${label}</span> </label> `, checkbox = dom.create(tpl); result.el = checkbox; return result; };
module.exports = function (result, args) { var name = result.name, options = args.options, field = dom.create(` <span class="input-label"> <ul class="editor-radios"> ${ createOptions(name, options) } </ul> </span> `); result.el = field; return result; };
function createOverlayFormEl(formLabel, innerEl) { var el = dom.create(` <section class="editor"> <form> <header> <span class="form-header">${formLabel}</span> <button type="submit" class="save">Save</button> </header> <div class="input-container"></div> </form> </section> `); dom.find(el, '.input-container').appendChild(innerEl); return el; }
module.exports = function (result, args) { var name = result.name, binders = result.binders, mode = args.mode, field = dom.create(`<label class="input-label"> <textarea class="codemirror" rv-field="${name}" rv-codemirror="${name}.data.value" data-codemirror-mode="${mode}" rv-value="${name}.data.value"></textarea> </label>`); // add the input to the field result.el = field; // add the binder binders.codemirror = initCodemirror(); return result; };
/** * Converts array of strings to option elements. * @param {array} items * @returns {Element} */ function formatOptions(items) { var optionsEl, options = flattenText(items).reduce(function (prev, curr) { return prev + '<option>' + curr + '</option>'; }, ''); optionsEl = dom.create(` <label> <select> ${ options } </select> </label> `); return optionsEl; }
module.exports = function (result, args) { var name = result.name, options = args.options, field; field = dom.create(` <div class="input-label"> <div class="checkbox-group"> ${ createOptions(name, options) } </div> </div> `); result.el = field; return result; };
module.exports = function (result, args) { var name = result.name, bindings = result.bindings, textArea; args = args || {}; bindings.placeholder = args.placeholder || ''; textArea = dom.create(` <label class="input-label"> <textarea class="editor-textarea" rv-field="${name}" ${setRequired(args.required)} rv-placeholder="${name}.placeholder" rv-value="${name}.data.value"></textarea> </label> `); result.el = textArea; return result; };
module.exports = function (result, args) { var el = result.el, name = result.name, tpl = ` <span class="soft-maxlength" data-maxlength="${args.value}" rv-remaining="${name}.data.value"></span> `, span = dom.create(tpl); result.binders.remaining = function (el, value) { var length = value ? cleanValue(value).length : 0, max = parseInt(el.getAttribute('data-maxlength')); setStyles(length, max, el); }; el.appendChild(span); return result; };
}).then((html) => create(html));
module.exports = function (result, args) { var name = result.name, allowRepeat = !!args && !!args.allowRepeatedItems, el = dom.create(` <div class="input-label"> <div rv-field="${name}" class="simple-list" rv-simplelist="${name}.data"> <span tabindex="0" rv-each-item="${name}.data" class="simple-list-item" rv-class-selected="item._selected" rv-on-click="${name}.selectItem" rv-on-keydown="${name}.keyactions">{ item.text }</span> <input class="simple-list-add" data-allow-repeat=${allowRepeat} rv-on-click="${name}.unselectAll" placeholder="Start typing here…" /> </div> </div>`); /** * unselect all items * @param {[]} items * @return {[]} */ function unselectAll(items) { return items.map(function (item) { item._selected = false; return item; }); } /** * unselect all items, then select a specific item * @param {object} item * @param {array} data */ function selectItem(item, data) { unselectAll(data); item._selected = true; } /** * select previous item in list * @param {Event} e * @param {number} index * @param {array} data */ function selectPrevious(e, index, data) { if (index > 0 && e.target.previousSibling) { selectItem(data[index - 1], data); e.target.previousSibling.focus(); } } /** * select next item in list * @param {Event} e * @param {number} index * @param {array} data */ function selectNext(e, index, data) { var input = dom.find(el, '.simple-list-add'); if (index < data.length - 1) { e.preventDefault(); // kill that tab! selectItem(data[index + 1], data ); e.target.nextSibling.focus(); } else { // we currently have the last item selected, so focus the input e.preventDefault(); e.stopPropagation(); // stop the current event first input.dispatchEvent(new Event('click')); input.focus(); } } /** * remove item from list * @param {Event} e * @param {number} index * @param {array} data */ function deleteItem(e, index, data) { var prevSibling = e.target.previousSibling, input = dom.find(dom.closest(e.target, '.simple-list'), '.simple-list-add'); e.preventDefault(); // prevent triggering the browser's back button data.splice(index, 1); // remove item from the list if (index > 0) { prevSibling.focus(); prevSibling.dispatchEvent(new Event('click')); } else { // you deleted all the items! focus the input input.focus(); } } /* Click Handlers */ // unselect all items when you click to add a new one result.bindings.unselectAll = function (e, bindings) { unselectAll(bindings[name].data); }; // select an item (and unselect all others) when you click it result.bindings.selectItem = function (e, bindings) { selectItem(bindings.item, bindings[name].data); }; // move between items and delete items when pressing the relevant keys (when an item is selected) result.bindings.keyactions = function (e, bindings) { var key = keycode(e), data = bindings[name].data, index = data.indexOf(bindings.item); if (key === 'left') { selectPrevious(e, index, data); } else if (key === 'tab' || key === 'right') { selectNext(e, index, data); } else if (key === 'delete' || key === 'backspace') { deleteItem(e, index, data); } }; // put the element into the result object result.el = el; // add binder for creating new items result.binders.simplelist = { publish: true, bind: function (boundEl) { // this is called when the binder initializes var addEl = dom.find(boundEl, '.simple-list-add'), allowRepeat = !!(addEl.getAttribute('data-allow-repeat') === 'true'), observer = this.observer; // check repeated items // returns true if repitition is disallowed and items repeat // returns false if repitition is allowed // returns false if repitition is disallowed and items don't repeat function hasRepeatedValue(value, data) { var oldItems = _.map(data, item => item.text.toLowerCase()); return !allowRepeat && _.contains(oldItems, value.toLowerCase()); } // add new item from the add-items field function addItem(e) { var data = observer.value(), newText = { text: addEl.value }, // get the new item text val = newText.text, repeated = hasRepeatedValue(val, data); // prevent creating newlines or tabbing out of the field if (e) { e.preventDefault(); } if (val.length && repeated) { addEl.setCustomValidity('Repeated items are not allowed!'); addEl.reportValidity(); } else if (val.length && !repeated) { addEl.setCustomValidity(''); // valid input addEl.value = ''; // remove it from the add-item field data.push(newText); // put it into the data observer.setValue(data); } else { addEl.setCustomValidity(''); // remove the invalidity of the input, so you can close the form // close the form focus.unfocus().catch(_.noop); } } // select the last item when you backspace from the add-items field function selectLastItem(e) { var data = observer.value(), newItems = dom.findAll(boundEl, '.simple-list-item'); if (!addEl.value || !addEl.value.length) { e.preventDefault(); // prevent triggering the browser's back button _.last(data)._selected = true; _.last(newItems).focus(); // focus on the last item observer.setValue(data); } } // handle keyboard events in the add-items field function handleItemKeyEvents(e) { var key = keycode(e); if (key === 'enter' || key === 'tab' || key === ',' && e.shiftKey === false) { addItem(e); } else if (key === 'delete' || key === 'backspace' || key === 'left') { selectLastItem(e); } } addEl.addEventListener('keydown', handleItemKeyEvents); } }; return result; };