app/assets/javascripts/spotlight/spotlight.js in blacklight-spotlight-3.5.0.4 vs app/assets/javascripts/spotlight/spotlight.js in blacklight-spotlight-3.6.0.beta1

- old
+ new

@@ -1,23 +1,7156 @@ -Spotlight = function() { - var buffer = []; - return { - onLoad: function(func) { - buffer.push(func); +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Spotlight = factory()); +})(this, (function () { 'use strict'; + + class Analytics { + connect() { + if (window._gaq != null) { + return _gaq.push(['_trackPageview']); + } else if (window.pageTracker != null) { + return pageTracker._trackPageview(); + } + } + } + + class BrowseGroupCateogries { + connect() { + var $container, slider; + + function init() { + var data = $container.data(); + var sidebar = $container.data().sidebar; + var items = data.browseGroupCategoriesCount; + var dir = $('html').attr('dir'); + var controls = $container.parent().find('.browse-group-categories-controls')[0]; + + slider = tns({ + container: $container[0], + controlsContainer: controls, + loop: false, + nav: false, + items: 1, + slideBy: 'page', + textDirection: dir, + responsive: { + 576: { + items: itemCount(items, sidebar) + } + } + }); + } + + // Destroy the slider instance, as tns will change the dom elements, causing some issues with turbolinks + function setupDestroy() { + document.addEventListener('turbolinks:before-cache', function() { + if (slider && slider.destroy) { + slider.destroy(); + } + }); + } + + function itemCount(items, sidebar) { + if (items < 3) { + return items; + } + return sidebar ? 3 : 4; + } + + return $('[data-browse-group-categories-carousel]').each(function() { + $container = $(this); + init(); + setupDestroy(); + }); + } + } + + class Carousel { + connect() { + $('.carousel').carousel(); + } + } + + class ClearFormButton { + connect() { + var $clearBtn = $('.btn-reset'); + var $input = $clearBtn.parent().prev('input'); + var btnCheck = function(){ + if ($input.val() !== '') { + $clearBtn.css('display', 'inline-block'); + } else { + $clearBtn.css('display', 'none'); + } + }; + + btnCheck(); + $input.on('keyup', function() { + btnCheck(); + }); + + $clearBtn.on('click', function(event) { + event.preventDefault(); + $input.val(''); + }); + } + } + + class ReportProblem { + connect(){ + var container, target; + + function init() { + const target_val = container.attr('data-target'); + if (!target_val) + return + + target = $("#" + target_val); + container.on('click', open); + target.find('[data-behavior="cancel-link"]').on('click', close); + } + + function open(event) { + event.preventDefault(); + target.slideToggle('slow'); + } + + function close(event) { + event.preventDefault(); + target.slideUp('fast'); + } + + return $('[data-behavior="contact-link"]').each(function() { + container = $(this); + init(); + }); + } + } + + class ZprLinks { + connect() { + $('.zpr-link').on('click', function() { + var modalDialog = $('#blacklight-modal .modal-dialog'); + var modalContent = modalDialog.find('.modal-content'); + modalDialog.removeClass('modal-lg'); + modalDialog.addClass('modal-xl'); + modalContent.html('<div id="osd-modal-container"></div>'); + var controls = `<div class="controls d-flex justify-content-center justify-content-md-end"> + <div class="custom-close-controls pr-3 pt-3"> + <button type="button" class="btn btn-dark" data-dismiss="modal" aria-hidden="true">${Spotlight.ZprLinks.close}</button> + </div> + <div class="zoom-controls mb-3 mr-md-3"> + <button id="osd-zoom-in" type="button" class="btn btn-dark">${Spotlight.ZprLinks.zoomIn}</button> + <button id="osd-zoom-out" type="button" class="btn btn-dark">${Spotlight.ZprLinks.zoomOut}</button> + </div> + <div id="empty-div-required-by-osd"></div> + </div>`; + + $('#osd-modal-container').append('<div id="osd-div"></div>'); + $('#osd-modal-container').append(controls); + + $('#blacklight-modal').modal('show'); + + $('#blacklight-modal').one('hidden.bs.modal', function (event) { + modalDialog.removeClass('modal-xl'); + modalDialog.addClass('modal-lg'); + }); + + OpenSeadragon({ + id: 'osd-div', + zoomInButton: "osd-zoom-in", + zoomOutButton: "osd-zoom-out", + // This is a hack where OpenSeadragon (if using mapped buttons) requires you + // to map all of the buttons. + homeButton: "empty-div-required-by-osd", + fullPageButton: "empty-div-required-by-osd", + nextButton: "empty-div-required-by-osd", + previousButton: "empty-div-required-by-osd", + tileSources: [$(this).data('iiif-tilesource')] + }); + }); + } + } + + class UserIndex { + connect() { + new Analytics().connect(); + new BrowseGroupCateogries().connect(); + new Carousel().connect(); + new ClearFormButton().connect(); + new ReportProblem().connect(); + new ZprLinks().connect(); + } + } + + /*! + * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/ + * Dual-licensed under the BSD or MIT licenses + */ + (function($, window, document, undefined$1) + { + var hasTouch = 'ontouchstart' in window; + + /** + * Detect CSS pointer-events property + * events are normally disabled on the dragging element to avoid conflicts + * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js + */ + var hasPointerEvents = (function() + { + var el = document.createElement('div'), + docEl = document.documentElement; + if (!('pointerEvents' in el.style)) { + return false; + } + el.style.pointerEvents = 'auto'; + el.style.pointerEvents = 'x'; + docEl.appendChild(el); + var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto'; + docEl.removeChild(el); + return !!supports; + })(); + + var eStart = hasTouch ? 'touchstart' : 'mousedown', + eMove = hasTouch ? 'touchmove' : 'mousemove', + eEnd = hasTouch ? 'touchend' : 'mouseup', + eCancel = hasTouch ? 'touchcancel' : 'mouseup'; + + var defaults = { + listNodeName : 'ol', + itemNodeName : 'li', + rootClass : 'dd', + listClass : 'dd-list', + itemClass : 'dd-item', + dragClass : 'dd-dragel', + handleClass : 'dd-handle', + collapsedClass : 'dd-collapsed', + placeClass : 'dd-placeholder', + noDragClass : 'dd-nodrag', + noChildrenClass : 'dd-nochildren', + emptyClass : 'dd-empty', + expandBtnHTML : '<button data-action="expand" type="button">Expand</button>', + collapseBtnHTML : '<button data-action="collapse" type="button">Collapse</button>', + group : 0, + maxDepth : 5, + threshold : 20, + reject : [], + //method for call when an item has been successfully dropped + //method has 1 argument in which sends an object containing all + //necessary details + dropCallback : null, + // When a node is dragged it is moved to its new location. + // You can set the next option to true to create a copy of the node that is dragged. + cloneNodeOnDrag : false, + // When the node is dragged and released outside its list delete it. + dragOutsideToDelete : false + }; + + function Plugin(element, options) + { + this.w = $(document); + this.el = $(element); + this.options = $.extend({}, defaults, options); + this.init(); + } + + Plugin.prototype = { + + init: function() + { + var list = this; + + list.reset(); + + list.el.data('nestable-group', this.options.group); + + list.placeEl = $('<div class="' + list.options.placeClass + '"/>'); + + $.each(this.el.find(list.options.itemNodeName), function(k, el) { + list.setParent($(el)); + }); + + list.el.on('click', 'button', function(e) + { + if (list.dragEl || (!hasTouch && e.button !== 0)) { + return; + } + var target = $(e.currentTarget), + action = target.data('action'), + item = target.parent(list.options.itemNodeName); + if (action === 'collapse') { + list.collapseItem(item); + } + if (action === 'expand') { + list.expandItem(item); + } + }); + + var onStartEvent = function(e) + { + var handle = $(e.target); + + list.nestableCopy = handle.closest('.'+list.options.rootClass).clone(true); + + if (!handle.hasClass(list.options.handleClass)) { + if (handle.closest('.' + list.options.noDragClass).length) { + return; + } + handle = handle.closest('.' + list.options.handleClass); + } + if (!handle.length || list.dragEl || (!hasTouch && e.which !== 1) || (hasTouch && e.touches.length !== 1)) { + return; + } + e.preventDefault(); + list.dragStart(hasTouch ? e.touches[0] : e); + }; + + var onMoveEvent = function(e) + { + if (list.dragEl) { + e.preventDefault(); + list.dragMove(hasTouch ? e.touches[0] : e); + } + }; + + var onEndEvent = function(e) + { + if (list.dragEl) { + e.preventDefault(); + list.dragStop(hasTouch ? e.touches[0] : e); + } + }; + + if (hasTouch) { + list.el[0].addEventListener(eStart, onStartEvent, false); + window.addEventListener(eMove, onMoveEvent, false); + window.addEventListener(eEnd, onEndEvent, false); + window.addEventListener(eCancel, onEndEvent, false); + } else { + list.el.on(eStart, onStartEvent); + list.w.on(eMove, onMoveEvent); + list.w.on(eEnd, onEndEvent); + } + + var destroyNestable = function() + { + if (hasTouch) { + list.el[0].removeEventListener(eStart, onStartEvent, false); + window.removeEventListener(eMove, onMoveEvent, false); + window.removeEventListener(eEnd, onEndEvent, false); + window.removeEventListener(eCancel, onEndEvent, false); + } else { + list.el.off(eStart, onStartEvent); + list.w.off(eMove, onMoveEvent); + list.w.off(eEnd, onEndEvent); + } + + list.el.off('click'); + list.el.unbind('destroy-nestable'); + + list.el.data("nestable", null); + + var buttons = list.el[0].getElementsByTagName('button'); + + $(buttons).remove(); + }; + + list.el.bind('destroy-nestable', destroyNestable); + }, + + destroy: function () + { + this.expandAll(); + this.el.trigger('destroy-nestable'); + }, + + serialize: function() + { + var data, + list = this; + const step = function(level, depth) + { + var array = [ ], + items = level.children(list.options.itemNodeName); + items.each(function() + { + var li = $(this), + item = $.extend({}, li.data()), + sub = li.children(list.options.listNodeName); + if (sub.length) { + item.children = step(sub); + } + array.push(item); + }); + return array; + }; + var el; + + if (list.el.is(list.options.listNodeName)) { + el = list.el; + } else { + el = list.el.find(list.options.listNodeName).first(); + } + data = step(el); + return data; + }, + + reset: function() + { + this.mouse = { + offsetX : 0, + offsetY : 0, + startX : 0, + startY : 0, + lastX : 0, + lastY : 0, + nowX : 0, + nowY : 0, + distX : 0, + distY : 0, + dirAx : 0, + dirX : 0, + dirY : 0, + lastDirX : 0, + lastDirY : 0, + distAxX : 0, + distAxY : 0 + }; + this.moving = false; + this.dragEl = null; + this.dragRootEl = null; + this.dragDepth = 0; + this.dragItem = null; + this.hasNewRoot = false; + this.pointEl = null; + this.sourceRoot = null; + this.isOutsideRoot = false; + }, + + expandItem: function(li) + { + li.removeClass(this.options.collapsedClass); + li.children('[data-action="expand"]').hide(); + li.children('[data-action="collapse"]').show(); + li.children(this.options.listNodeName).show(); + this.el.trigger('expand', [li]); + li.trigger('expand'); + }, + + collapseItem: function(li) + { + var lists = li.children(this.options.listNodeName); + if (lists.length) { + li.addClass(this.options.collapsedClass); + li.children('[data-action="collapse"]').hide(); + li.children('[data-action="expand"]').show(); + li.children(this.options.listNodeName).hide(); + } + this.el.trigger('collapse', [li]); + li.trigger('collapse'); + }, + + expandAll: function() + { + var list = this; + list.el.find(list.options.itemNodeName).each(function() { + list.expandItem($(this)); + }); + }, + + collapseAll: function() + { + var list = this; + list.el.find(list.options.itemNodeName).each(function() { + list.collapseItem($(this)); + }); + }, + + setParent: function(li) + { + if (li.children(this.options.listNodeName).length) { + li.prepend($(this.options.expandBtnHTML)); + li.prepend($(this.options.collapseBtnHTML)); + } + if( (' ' + li[0].className + ' ').indexOf(' ' + defaults.collapsedClass + ' ') > -1 ) + { + li.children('[data-action="collapse"]').hide(); + } else { + li.children('[data-action="expand"]').hide(); + } + }, + + unsetParent: function(li) + { + li.removeClass(this.options.collapsedClass); + li.children('[data-action]').remove(); + li.children(this.options.listNodeName).remove(); + }, + + dragStart: function(e) + { + var mouse = this.mouse, + target = $(e.target), + dragItem = target.closest('.' + this.options.handleClass).closest(this.options.itemNodeName); + + this.sourceRoot = target.closest('.' + this.options.rootClass); + + this.dragItem = dragItem; + + this.placeEl.css('height', dragItem.height()); + + mouse.offsetX = e.offsetX !== undefined$1 ? e.offsetX : e.pageX - target.offset().left; + mouse.offsetY = e.offsetY !== undefined$1 ? e.offsetY : e.pageY - target.offset().top; + mouse.startX = mouse.lastX = e.pageX; + mouse.startY = mouse.lastY = e.pageY; + + this.dragRootEl = this.el; + + this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass); + this.dragEl.css('width', dragItem.width()); + + // fix for zepto.js + //dragItem.after(this.placeEl).detach().appendTo(this.dragEl); + if(this.options.cloneNodeOnDrag) { + dragItem.after(dragItem.clone()); + } else { + dragItem.after(this.placeEl); + } + dragItem[0].parentNode.removeChild(dragItem[0]); + dragItem.appendTo(this.dragEl); + + $(document.body).append(this.dragEl); + this.dragEl.css({ + 'left' : e.pageX - mouse.offsetX, + 'top' : e.pageY - mouse.offsetY + }); + // total depth of dragging item + var i, depth, + items = this.dragEl.find(this.options.itemNodeName); + for (i = 0; i < items.length; i++) { + depth = $(items[i]).parents(this.options.listNodeName).length; + if (depth > this.dragDepth) { + this.dragDepth = depth; + } + } + }, + + dragStop: function(e) + { + // fix for zepto.js + //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach()); + var el = this.dragEl.children(this.options.itemNodeName).first(); + el[0].parentNode.removeChild(el[0]); + + if(this.isOutsideRoot && this.options.dragOutsideToDelete) + { + var parent = this.placeEl.parent(); + this.placeEl.remove(); + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + // If all nodes where deleted, create a placeholder element. + if (!this.dragRootEl.find(this.options.itemNodeName).length) + { + this.dragRootEl.append('<div class="' + this.options.emptyClass + '"/>'); + } + } + else + { + this.placeEl.replaceWith(el); + } + + if (!this.moving) + { + $(this.dragItem).trigger('click'); + } + + var i; + var isRejected = false; + for (i = 0; i < this.options.reject.length; i++) + { + var reject = this.options.reject[i]; + if (reject.rule.apply(this.dragRootEl)) + { + var nestableDragEl = el.clone(true); + this.dragRootEl.html(this.nestableCopy.children().clone(true)); + if (reject.action) { + reject.action.apply(this.dragRootEl, [nestableDragEl]); + } + + isRejected = true; + break; + } + } + + if (!isRejected) + { + this.dragEl.remove(); + this.el.trigger('change'); + + //Let's find out new parent id + var parentItem = el.parent().parent(); + var parentId = null; + if(parentItem !== null && !parentItem.is('.' + this.options.rootClass)) + parentId = parentItem.data('id'); + + if($.isFunction(this.options.dropCallback)) + { + var details = { + sourceId : el.data('id'), + destId : parentId, + sourceEl : el, + destParent : parentItem, + destRoot : el.closest('.' + this.options.rootClass), + sourceRoot : this.sourceRoot + }; + this.options.dropCallback.call(this, details); + } + + if (this.hasNewRoot) { + this.dragRootEl.trigger('change'); + } + + this.reset(); + } + }, + + dragMove: function(e) + { + var list, parent, prev, next, depth, + opt = this.options, + mouse = this.mouse; + + this.dragEl.css({ + 'left' : e.pageX - mouse.offsetX, + 'top' : e.pageY - mouse.offsetY + }); + + // mouse position last events + mouse.lastX = mouse.nowX; + mouse.lastY = mouse.nowY; + // mouse position this events + mouse.nowX = e.pageX; + mouse.nowY = e.pageY; + // distance mouse moved between events + mouse.distX = mouse.nowX - mouse.lastX; + mouse.distY = mouse.nowY - mouse.lastY; + // direction mouse was moving + mouse.lastDirX = mouse.dirX; + mouse.lastDirY = mouse.dirY; + // direction mouse is now moving (on both axis) + mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1; + mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1; + // axis mouse is now moving on + var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0; + + // do nothing on first move + if (!this.moving) { + mouse.dirAx = newAx; + this.moving = true; + return; + } + + // calc distance moved on this axis (and direction) + if (mouse.dirAx !== newAx) { + mouse.distAxX = 0; + mouse.distAxY = 0; + } else { + mouse.distAxX += Math.abs(mouse.distX); + if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) { + mouse.distAxX = 0; + } + mouse.distAxY += Math.abs(mouse.distY); + if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) { + mouse.distAxY = 0; + } + } + mouse.dirAx = newAx; + + /** + * move horizontal + */ + if (mouse.dirAx && mouse.distAxX >= opt.threshold) { + // reset move distance on x-axis for new phase + mouse.distAxX = 0; + prev = this.placeEl.prev(opt.itemNodeName); + // increase horizontal level if previous sibling exists and is not collapsed + if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass) && !prev.hasClass(opt.noChildrenClass)) { + // cannot increase level when item above is collapsed + list = prev.find(opt.listNodeName).last(); + // check if depth limit has reached + depth = this.placeEl.parents(opt.listNodeName).length; + if (depth + this.dragDepth <= opt.maxDepth) { + // create new sub-level if one doesn't exist + if (!list.length) { + list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass); + list.append(this.placeEl); + prev.append(list); + this.setParent(prev); + } else { + // else append to next level up + list = prev.children(opt.listNodeName).last(); + list.append(this.placeEl); + } + } + } + // decrease horizontal level + if (mouse.distX < 0) { + // we can't decrease a level if an item preceeds the current one + next = this.placeEl.next(opt.itemNodeName); + if (!next.length) { + parent = this.placeEl.parent(); + this.placeEl.closest(opt.itemNodeName).after(this.placeEl); + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + } + } + } + + var isEmpty = false; + + // find list item under cursor + if (!hasPointerEvents) { + this.dragEl[0].style.visibility = 'hidden'; + } + + this.pointEl = $(document.elementFromPoint(e.pageX - document.documentElement.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop))); + + // Check if the node is dragged outside of its list. + if(this.dragRootEl.has(this.pointEl).length) { + this.isOutsideRoot = false; + this.dragEl[0].style.opacity = 1; + } else { + this.isOutsideRoot = true; + this.dragEl[0].style.opacity = 0.5; + } + + // find parent list of item under cursor + var pointElRoot = this.pointEl.closest('.' + opt.rootClass), + isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id'); + + this.isOutsideRoot = !pointElRoot.length; + + if (!hasPointerEvents) { + this.dragEl[0].style.visibility = 'visible'; + } + if (this.pointEl.hasClass(opt.handleClass)) { + this.pointEl = this.pointEl.closest( opt.itemNodeName ); + } + + if (opt.maxDepth == 1 && !this.pointEl.hasClass(opt.itemClass)) { + this.pointEl = this.pointEl.closest("." + opt.itemClass); + } + + if (this.pointEl.hasClass(opt.emptyClass)) { + isEmpty = true; + } + else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) { + return; + } + + /** + * move vertical + */ + if (!mouse.dirAx || isNewRoot || isEmpty) { + // check if groups match if dragging over new root + if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) { + return; + } + // check depth limit + depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length; + if (depth > opt.maxDepth) { + return; + } + var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2); + parent = this.placeEl.parent(); + // if empty create new list to replace empty placeholder + if (isEmpty) { + list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass); + list.append(this.placeEl); + this.pointEl.replaceWith(list); + } + else if (before) { + this.pointEl.before(this.placeEl); + } + else { + this.pointEl.after(this.placeEl); + } + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + if (!this.dragRootEl.find(opt.itemNodeName).length) { + this.dragRootEl.append('<div class="' + opt.emptyClass + '"/>'); + } + // parent root list has changed + this.dragRootEl = pointElRoot; + if (isNewRoot) { + this.hasNewRoot = this.el[0] !== this.dragRootEl[0]; + } + } + } + + }; + + $.fn.nestable = function(params) + { + var lists = this, + retval = this; + + var generateUid = function (separator) { + var delim = separator || "-"; + + function S4() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + } + + return (S4() + S4() + delim + S4() + delim + S4() + delim + S4() + delim + S4() + S4() + S4()); + }; + + lists.each(function() + { + var plugin = $(this).data("nestable"); + + if (!plugin) { + $(this).data("nestable", new Plugin(this, params)); + $(this).data("nestable-id", generateUid()); + } else { + if (typeof params === 'string' && typeof plugin[params] === 'function') { + retval = plugin[params](); + } + } + }); + + return retval || lists; + }; + + })(window.jQuery || window.Zepto, window, document); + + /* + https://gist.github.com/pjambet/3710461 + */ + var LATIN_MAP = { + 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': + 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', + 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': + 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', + 'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä': + 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', + 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': + 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', + 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' + }; + var LATIN_SYMBOLS_MAP = { + '©':'(c)' + }; + var GREEK_MAP = { + 'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8', + 'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p', + 'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w', + 'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s', + 'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i', + 'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8', + 'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P', + 'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W', + 'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I', + 'Ϋ':'Y' + }; + var TURKISH_MAP = { + 'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U', + 'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G' + }; + var RUSSIAN_MAP = { + 'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', + 'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', + 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c', + 'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', + 'я':'ya', + 'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', + 'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', + 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C', + 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', + 'Я':'Ya' + }; + var UKRAINIAN_MAP = { + 'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g' + }; + var CZECH_MAP = { + 'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u', + 'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T', + 'Ů':'U', 'Ž':'Z' + }; + + var POLISH_MAP = { + 'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z', + 'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'e', 'Ł':'L', 'Ń':'N', 'Ó':'o', 'Ś':'S', + 'Ź':'Z', 'Ż':'Z' + }; + + var LATVIAN_MAP = { + 'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n', + 'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'i', + 'Ķ':'k', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'u', 'Ž':'Z' + }; + + var ALL_DOWNCODE_MAPS=new Array(); + ALL_DOWNCODE_MAPS[0]=LATIN_MAP; + ALL_DOWNCODE_MAPS[1]=LATIN_SYMBOLS_MAP; + ALL_DOWNCODE_MAPS[2]=GREEK_MAP; + ALL_DOWNCODE_MAPS[3]=TURKISH_MAP; + ALL_DOWNCODE_MAPS[4]=RUSSIAN_MAP; + ALL_DOWNCODE_MAPS[5]=UKRAINIAN_MAP; + ALL_DOWNCODE_MAPS[6]=CZECH_MAP; + ALL_DOWNCODE_MAPS[7]=POLISH_MAP; + ALL_DOWNCODE_MAPS[8]=LATVIAN_MAP; + + var Downcoder = new Object(); + Downcoder.Initialize = function() + { + if (Downcoder.map) // already made + return ; + Downcoder.map ={}; + Downcoder.chars = '' ; + for(var i in ALL_DOWNCODE_MAPS) + { + var lookup = ALL_DOWNCODE_MAPS[i]; + for (var c in lookup) + { + Downcoder.map[c] = lookup[c] ; + Downcoder.chars += c ; + } + } + Downcoder.regex = new RegExp('[' + Downcoder.chars + ']|[^' + Downcoder.chars + ']+','g') ; + }; + + /* From https://github.com/TimSchlechter/bootstrap-tagsinput/blob/2661784c2c281d3a69b93897ff3f39e4ffa5cbd1/dist/bootstrap-tagsinput.js */ + + /* The MIT License (MIT) + + Copyright (c) 2013 Tim Schlechter + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + /* Retrieved 12 February 2014 */ + + (function ($) { + + var defaultOptions = { + tagClass: function(item) { + return 'badge badge-info'; + }, + itemValue: function(item) { + return item ? item.toString() : item; + }, + itemText: function(item) { + return this.itemValue(item); + }, + freeInput: true, + maxTags: undefined, + confirmKeys: [13], + onTagExists: function(item, $tag) { + $tag.hide().fadeIn(); + } + }; + + /** + * Constructor function + */ + function TagsInput(element, options) { + this.itemsArray = []; + + this.$element = $(element); + this.$element.hide(); + + this.isSelect = (element.tagName === 'SELECT'); + this.multiple = (this.isSelect && element.hasAttribute('multiple')); + this.objectItems = options && options.itemValue; + this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; + this.inputSize = Math.max(1, this.placeholderText.length); + + this.$container = $('<div class="bootstrap-tagsinput"></div>'); + this.$input = $('<input size="' + this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container); + + this.$element.after(this.$container); + + this.build(options); + } + + TagsInput.prototype = { + constructor: TagsInput, + + /** + * Adds the given item as a new tag. Pass true to dontPushVal to prevent + * updating the elements val() + */ + add: function(item, dontPushVal) { + var self = this; + + if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) + return; + + // Ignore falsey values, except false + if (item !== false && !item) + return; + + // Throw an error when trying to add an object while the itemValue option was not set + if (typeof item === "object" && !self.objectItems) + throw("Can't add objects when itemValue option is not set"); + + // Ignore strings only containg whitespace + if (item.toString().match(/^\s*$/)) + return; + + // If SELECT but not multiple, remove current tag + if (self.isSelect && !self.multiple && self.itemsArray.length > 0) + self.remove(self.itemsArray[0]); + + if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { + var items = item.split(','); + if (items.length > 1) { + for (var i = 0; i < items.length; i++) { + this.add(items[i], true); + } + + if (!dontPushVal) + self.pushVal(); + return; + } + } + + var itemValue = self.options.itemValue(item), + itemText = self.options.itemText(item), + tagClass = self.options.tagClass(item); + + // Ignore items allready added + var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; + if (existing) { + // Invoke onTagExists + if (self.options.onTagExists) { + var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); + self.options.onTagExists(item, $existingTag); + } + return; + } + + // register item in internal array and map + self.itemsArray.push(item); + + // add a tag element + var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>'); + $tag.data('item', item); + self.findInputWrapper().before($tag); + $tag.after(' '); + + // add <option /> if item represents a value not present in one of the <select />'s options + if (self.isSelect && !$('option[value="' + escape(itemValue) + '"]',self.$element)[0]) { + var $option = $('<option selected>' + htmlEncode(itemText) + '</option>'); + $option.data('item', item); + $option.attr('value', itemValue); + self.$element.append($option); + } + + if (!dontPushVal) + self.pushVal(); + + // Add class when reached maxTags + if (self.options.maxTags === self.itemsArray.length) + self.$container.addClass('bootstrap-tagsinput-max'); + + self.$element.trigger($.Event('itemAdded', { item: item })); + }, + + /** + * Removes the given item. Pass true to dontPushVal to prevent updating the + * elements val() + */ + remove: function(item, dontPushVal) { + var self = this; + + if (self.objectItems) { + if (typeof item === "object") + item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } )[0]; + else + item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } )[0]; + } + + if (item) { + $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove(); + $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove(); + self.itemsArray.splice($.inArray(item, self.itemsArray), 1); + } + + if (!dontPushVal) + self.pushVal(); + + // Remove class when reached maxTags + if (self.options.maxTags > self.itemsArray.length) + self.$container.removeClass('bootstrap-tagsinput-max'); + + self.$element.trigger($.Event('itemRemoved', { item: item })); + }, + + /** + * Removes all items + */ + removeAll: function() { + var self = this; + + $('.tag', self.$container).remove(); + $('option', self.$element).remove(); + + while(self.itemsArray.length > 0) + self.itemsArray.pop(); + + self.pushVal(); + + if (self.options.maxTags && !this.isEnabled()) + this.enable(); + }, + + /** + * Refreshes the tags so they match the text/value of their corresponding + * item. + */ + refresh: function() { + var self = this; + $('.tag', self.$container).each(function() { + var $tag = $(this), + item = $tag.data('item'), + itemValue = self.options.itemValue(item), + itemText = self.options.itemText(item), + tagClass = self.options.tagClass(item); + + // Update tag's class and inner text + $tag.attr('class', null); + $tag.addClass('tag ' + htmlEncode(tagClass)); + $tag.contents().filter(function() { + return this.nodeType == 3; + })[0].nodeValue = htmlEncode(itemText); + + if (self.isSelect) { + var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; }); + option.attr('value', itemValue); + } + }); + }, + + /** + * Returns the items added as tags + */ + items: function() { + return this.itemsArray; + }, + + /** + * Assembly value by retrieving the value of each item, and set it on the + * element. + */ + pushVal: function() { + var self = this, + val = $.map(self.items(), function(item) { + return self.options.itemValue(item).toString(); + }); + + self.$element.val(val, true).trigger('change'); + }, + + /** + * Initializes the tags input behaviour on the element + */ + build: function(options) { + var self = this; + + self.options = $.extend({}, defaultOptions, options); + var typeahead = self.options.typeahead || {}; + + // When itemValue is set, freeInput should always be false + if (self.objectItems) + self.options.freeInput = false; + + makeOptionItemFunction(self.options, 'itemValue'); + makeOptionItemFunction(self.options, 'itemText'); + makeOptionItemFunction(self.options, 'tagClass'); + + // for backwards compatibility, self.options.source is deprecated + if (self.options.source) + typeahead.source = self.options.source; + + if (typeahead.source && $.fn.typeahead) { + makeOptionFunction(typeahead, 'source'); + + self.$input.typeahead({ + source: function (query, process) { + function processItems(items) { + var texts = []; + + for (var i = 0; i < items.length; i++) { + var text = self.options.itemText(items[i]); + map[text] = items[i]; + texts.push(text); + } + process(texts); + } + + this.map = {}; + var map = this.map, + data = typeahead.source(query); + + if ($.isFunction(data.success)) { + // support for Angular promises + data.success(processItems); + } else { + // support for functions and jquery promises + $.when(data) + .then(processItems); + } + }, + updater: function (text) { + self.add(this.map[text]); + }, + matcher: function (text) { + return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1); + }, + sorter: function (texts) { + return texts.sort(); + }, + highlighter: function (text) { + var regex = new RegExp( '(' + this.query + ')', 'gi' ); + return text.replace( regex, "<strong>$1</strong>" ); + } + }); + } + + self.$container.on('click', $.proxy(function(event) { + self.$input.focus(); + }, self)); + + self.$container.on('keydown', 'input', $.proxy(function(event) { + var $input = $(event.target), + $inputWrapper = self.findInputWrapper(); + + switch (event.which) { + // BACKSPACE + case 8: + if (doGetCaretPosition($input[0]) === 0) { + var prev = $inputWrapper.prev(); + if (prev) { + self.remove(prev.data('item')); + } + } + break; + + // DELETE + case 46: + if (doGetCaretPosition($input[0]) === 0) { + var next = $inputWrapper.next(); + if (next) { + self.remove(next.data('item')); + } + } + break; + + // LEFT ARROW + case 37: + // Try to move the input before the previous tag + var $prevTag = $inputWrapper.prev(); + if ($input.val().length === 0 && $prevTag[0]) { + $prevTag.before($inputWrapper); + $input.focus(); + } + break; + // RIGHT ARROW + case 39: + // Try to move the input after the next tag + var $nextTag = $inputWrapper.next(); + if ($input.val().length === 0 && $nextTag[0]) { + $nextTag.after($inputWrapper); + $input.focus(); + } + break; + default: + // When key corresponds one of the confirmKeys, add current input + // as a new tag + if (self.options.freeInput && $.inArray(event.which, self.options.confirmKeys) >= 0) { + self.add($input.val()); + $input.val(''); + event.preventDefault(); + } + } + + // Reset internal input's size + $input.attr('size', Math.max(this.inputSize, $input.val().length)); + }, self)); + + // Remove icon clicked + self.$container.on('click', '[data-role=remove]', $.proxy(function(event) { + self.remove($(event.target).closest('.tag').data('item')); + }, self)); + + // Only add existing value as tags when using strings as tags + if (self.options.itemValue === defaultOptions.itemValue) { + if (self.$element[0].tagName === 'INPUT') { + self.add(self.$element.val()); + } else { + $('option', self.$element).each(function() { + self.add($(this).attr('value'), true); + }); + } + } + }, + + /** + * Removes all tagsinput behaviour and unregsiter all event handlers + */ + destroy: function() { + var self = this; + + // Unbind events + self.$container.off('keypress', 'input'); + self.$container.off('click', '[role=remove]'); + + self.$container.remove(); + self.$element.removeData('tagsinput'); + self.$element.show(); + }, + + /** + * Sets focus on the tagsinput + */ + focus: function() { + this.$input.focus(); + }, + + /** + * Returns the internal input element + */ + input: function() { + return this.$input; + }, + + /** + * Returns the element which is wrapped around the internal input. This + * is normally the $container, but typeahead.js moves the $input element. + */ + findInputWrapper: function() { + var elt = this.$input[0], + container = this.$container[0]; + while(elt && elt.parentNode !== container) + elt = elt.parentNode; + + return $(elt); + } + }; + + /** + * Register JQuery plugin + */ + $.fn.tagsinput = function(arg1, arg2) { + var results = []; + + this.each(function() { + var tagsinput = $(this).data('tagsinput'); + + // Initialize a new tags input + if (!tagsinput) { + tagsinput = new TagsInput(this, arg1); + $(this).data('tagsinput', tagsinput); + results.push(tagsinput); + + if (this.tagName === 'SELECT') { + $('option', $(this)).attr('selected', 'selected'); + } + + // Init tags from $(this).val() + $(this).val($(this).val()); + } else { + // Invoke function on existing tags input + var retVal = tagsinput[arg1](arg2); + if (retVal !== undefined) + results.push(retVal); + } + }); + + if ( typeof arg1 == 'string') { + // Return the results from the invoked function calls + return results.length > 1 ? results : results[0]; + } else { + return results; + } + }; + + $.fn.tagsinput.Constructor = TagsInput; + + /** + * Most options support both a string or number as well as a function as + * option value. This function makes sure that the option with the given + * key in the given options is wrapped in a function + */ + function makeOptionItemFunction(options, key) { + if (typeof options[key] !== 'function') { + var propertyName = options[key]; + options[key] = function(item) { return item[propertyName]; }; + } + } + function makeOptionFunction(options, key) { + if (typeof options[key] !== 'function') { + var value = options[key]; + options[key] = function() { return value; }; + } + } + /** + * HtmlEncodes the given value + */ + var htmlEncodeContainer = $('<div />'); + function htmlEncode(value) { + if (value) { + return htmlEncodeContainer.text(value).html(); + } else { + return ''; + } + } + + /** + * Returns the position of the caret in the given input field + * http://flightschool.acylt.com/devnotes/caret-position-woes/ + */ + function doGetCaretPosition(oField) { + var iCaretPos = 0; + if (document.selection) { + oField.focus (); + var oSel = document.selection.createRange(); + oSel.moveStart ('character', -oField.value.length); + iCaretPos = oSel.text.length; + } else if (oField.selectionStart || oField.selectionStart == '0') { + iCaretPos = oField.selectionStart; + } + return (iCaretPos); + } + + /** + * Initialize tagsinput behaviour on inputs and selects which have + * data-role=tagsinput + */ + $(function() { + $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput(); + }); + })(window.jQuery); + + /*! + SerializeJSON jQuery plugin. + https://github.com/marioizquierdo/jquery.serializeJSON + version 2.4.2 (Oct, 2014) + + Copyright (c) 2014 Mario Izquierdo + Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + */ + (function ($) { + + // jQuery('form').serializeJSON() + $.fn.serializeJSON = function (options) { + var serializedObject, formAsArray, keys, type, value, f, opts; + f = $.serializeJSON; + opts = f.optsWithDefaults(options); // calculate values for options {parseNumbers, parseBoolens, parseNulls} + f.validateOptions(opts); + formAsArray = this.serializeArray(); // array of objects {name, value} + f.readCheckboxUncheckedValues(formAsArray, this, opts); // add {name, value} of unchecked checkboxes if needed + + serializedObject = {}; + $.each(formAsArray, function (i, input) { + keys = f.splitInputNameIntoKeysArray(input.name); + type = keys.pop(); // the last element is always the type ("string" by default) + if (type !== 'skip') { // easy way to skip a value + value = f.parseValue(input.value, type, opts); // string, number, boolean or null + if (opts.parseWithFunction && type === '_') value = opts.parseWithFunction(value, input.name); // allow for custom parsing + f.deepSet(serializedObject, keys, value, opts); + } + }); + return serializedObject; + }; + + // Use $.serializeJSON as namespace for the auxiliar functions + // and to define defaults + $.serializeJSON = { + + defaultOptions: { + parseNumbers: false, // convert values like "1", "-2.33" to 1, -2.33 + parseBooleans: false, // convert "true", "false" to true, false + parseNulls: false, // convert "null" to null + parseAll: false, // all of the above + parseWithFunction: null, // to use custom parser, a function like: function(val){ return parsed_val; } + checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them) + useIntKeysAsArrayIndex: false // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]} + }, + + // Merge options with defaults to get {parseNumbers, parseBoolens, parseNulls, useIntKeysAsArrayIndex} + optsWithDefaults: function(options) { + var f, parseAll; + if (options == null) options = {}; // arg default value = {} + f = $.serializeJSON; + parseAll = f.optWithDefaults('parseAll', options); + return { + parseNumbers: parseAll || f.optWithDefaults('parseNumbers', options), + parseBooleans: parseAll || f.optWithDefaults('parseBooleans', options), + parseNulls: parseAll || f.optWithDefaults('parseNulls', options), + parseWithFunction: f.optWithDefaults('parseWithFunction', options), + checkboxUncheckedValue: f.optWithDefaults('checkboxUncheckedValue', options), + useIntKeysAsArrayIndex: f.optWithDefaults('useIntKeysAsArrayIndex', options) + } + }, + + optWithDefaults: function(key, options) { + return (options[key] !== false) && (options[key] !== '') && (options[key] || $.serializeJSON.defaultOptions[key]); + }, + + validateOptions: function(opts) { + var opt, validOpts; + validOpts = ['parseNumbers', 'parseBooleans', 'parseNulls', 'parseAll', 'parseWithFunction', 'checkboxUncheckedValue', 'useIntKeysAsArrayIndex']; + for (opt in opts) { + if (validOpts.indexOf(opt) === -1) { + throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(',')); + } + } + }, + + // Convert the string to a number, boolean or null, depending on the enable option and the string format. + parseValue: function(str, type, opts) { + var f; + f = $.serializeJSON; + if (type == 'string') return str; // force string + if (type == 'number' || (opts.parseNumbers && f.isNumeric(str))) return Number(str); // number + if (type == 'boolean' || (opts.parseBooleans && (str === "true" || str === "false"))) return (["false", "null", "undefined", "", "0"].indexOf(str) === -1); // boolean + if (type == 'null' || (opts.parseNulls && str == "null")) return ["false", "null", "undefined", "", "0"].indexOf(str) !== -1 ? null : str; // null + if (type == 'array' || type == 'object') return JSON.parse(str); // array or objects require JSON + if (type == 'auto') return f.parseValue(str, null, {parseNumbers: true, parseBooleans: true, parseNulls: true}); // try again with something like "parseAll" + return str; // otherwise, keep same string + }, + + isObject: function(obj) { return obj === Object(obj); }, // is this variable an object? + isUndefined: function(obj) { return obj === void 0; }, // safe check for undefined values + isValidArrayIndex: function(val) { return /^[0-9]+$/.test(String(val)); }, // 1,2,3,4 ... are valid array indexes + isNumeric: function(obj) { return obj - parseFloat(obj) >= 0; }, // taken from jQuery.isNumeric implementation. Not using jQuery.isNumeric to support old jQuery and Zepto versions + + // Split the input name in programatically readable keys. + // The last element is always the type (default "_"). + // Examples: + // "foo" => ['foo', '_'] + // "foo:string" => ['foo', 'string'] + // "foo:boolean" => ['foo', 'boolean'] + // "[foo]" => ['foo', '_'] + // "foo[inn][bar]" => ['foo', 'inn', 'bar', '_'] + // "foo[inn[bar]]" => ['foo', 'inn', 'bar', '_'] + // "foo[inn][arr][0]" => ['foo', 'inn', 'arr', '0', '_'] + // "arr[][val]" => ['arr', '', 'val', '_'] + // "arr[][val]:null" => ['arr', '', 'val', 'null'] + splitInputNameIntoKeysArray: function (name) { + var keys, nameWithoutType, type, _ref, f; + f = $.serializeJSON; + _ref = f.extractTypeFromInputName(name), nameWithoutType = _ref[0], type = _ref[1]; + keys = nameWithoutType.split('['); // split string into array + keys = $.map(keys, function (key) { return key.replace(/]/g, ''); }); // remove closing brackets + if (keys[0] === '') { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]") + keys.push(type); // add type at the end + return keys; + }, + + // Returns [name-without-type, type] from name. + // "foo" => ["foo", "_"] + // "foo:boolean" => ["foo", "boolean"] + // "foo[bar]:null" => ["foo[bar]", "null"] + extractTypeFromInputName: function(name) { + var match; + $.serializeJSON; + if (match = name.match(/(.*):([^:]+)$/)){ + var validTypes = ['string', 'number', 'boolean', 'null', 'array', 'object', 'skip', 'auto']; // validate type + if (validTypes.indexOf(match[2]) !== -1) { + return [match[1], match[2]]; + } else { + throw new Error("serializeJSON ERROR: Invalid type " + match[2] + " found in input name '" + name + "', please use one of " + validTypes.join(', ')) + } + } else { + return [name, '_']; // no defined type, then use parse options + } + }, + + // Set a value in an object or array, using multiple keys to set in a nested object or array: + // + // deepSet(obj, ['foo'], v) // obj['foo'] = v + // deepSet(obj, ['foo', 'inn'], v) // obj['foo']['inn'] = v // Create the inner obj['foo'] object, if needed + // deepSet(obj, ['foo', 'inn', '123'], v) // obj['foo']['arr']['123'] = v // + // + // deepSet(obj, ['0'], v) // obj['0'] = v + // deepSet(arr, ['0'], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v + // deepSet(arr, [''], v) // arr.push(v) + // deepSet(obj, ['arr', ''], v) // obj['arr'].push(v) + // + // arr = []; + // deepSet(arr, ['', v] // arr => [v] + // deepSet(arr, ['', 'foo'], v) // arr => [v, {foo: v}] + // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}] + // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}, {bar: v}] + // + deepSet: function (o, keys, value, opts) { + var key, nextKey, tail, lastIdx, lastVal, f; + if (opts == null) opts = {}; + f = $.serializeJSON; + if (f.isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); } + if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); } + + key = keys[0]; + + // Only one key, then it's not a deepSet, just assign the value. + if (keys.length === 1) { + if (key === '') { + o.push(value); // '' is used to push values into the array (assume o is an array) + } else { + o[key] = value; // other keys can be used as object keys or array indexes + } + + // With more keys is a deepSet. Apply recursively. + } else { + + nextKey = keys[1]; + + // '' is used to push values into the array, + // with nextKey, set the value into the same object, in object[nextKey]. + // Covers the case of ['', 'foo'] and ['', 'var'] to push the object {foo, var}, and the case of nested arrays. + if (key === '') { + lastIdx = o.length - 1; // asume o is array + lastVal = o[lastIdx]; + if (f.isObject(lastVal) && (f.isUndefined(lastVal[nextKey]) || keys.length > 2)) { // if nextKey is not present in the last object element, or there are more keys to deep set + key = lastIdx; // then set the new value in the same object element + } else { + key = lastIdx + 1; // otherwise, point to set the next index in the array + } + } + + // o[key] defaults to object or array, depending if nextKey is an array index (int or '') or an object key (string) + if (f.isUndefined(o[key])) { + if (nextKey === '') { // '' is used to push values into the array. + o[key] = []; + } else if (opts.useIntKeysAsArrayIndex && f.isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index + o[key] = []; + } else { // for anything else, use an object, where nextKey is going to be the attribute name + o[key] = {}; + } + } + + // Recursively set the inner object + tail = keys.slice(1); + f.deepSet(o[key], tail, value, opts); + } + }, + + // Fill the formAsArray object with values for the unchecked checkbox inputs, + // using the same format as the jquery.serializeArray function. + // The value of the unchecked values is determined from the opts.checkboxUncheckedValue + // and/or the data-unchecked-value attribute of the inputs. + readCheckboxUncheckedValues: function (formAsArray, $form, opts) { + var selector, $uncheckedCheckboxes, $el, dataUncheckedValue, f; + if (opts == null) opts = {}; + f = $.serializeJSON; + + selector = 'input[type=checkbox][name]:not(:checked,[disabled])'; + $uncheckedCheckboxes = $form.find(selector).add($form.filter(selector)); + $uncheckedCheckboxes.each(function (i, el) { + $el = $(el); + dataUncheckedValue = $el.attr('data-unchecked-value'); + if(dataUncheckedValue) { // data-unchecked-value has precedence over option opts.checkboxUncheckedValue + formAsArray.push({name: el.name, value: dataUncheckedValue}); + } else { + if (!f.isUndefined(opts.checkboxUncheckedValue)) { + formAsArray.push({name: el.name, value: opts.checkboxUncheckedValue}); + } + } + }); + } + + }; + + }(window.jQuery || window.Zepto || window.$)); + + /* + * Leaflet-IIIF 3.0.0 + * IIIF Viewer for Leaflet + * by Jack Reed, @mejackreed + */ + + L.TileLayer.Iiif = L.TileLayer.extend({ + options: { + continuousWorld: true, + tileSize: 256, + updateWhenIdle: true, + tileFormat: 'jpg', + fitBounds: true, + setMaxBounds: false }, - activate: function() { - for(var i = 0; i < buffer.length; i++) { - buffer[i].call(); + initialize: function(url, options) { + options = typeof options !== 'undefined' ? options : {}; + + if (options.maxZoom) { + this._customMaxZoom = true; } + + // Check for explicit tileSize set + if (options.tileSize) { + this._explicitTileSize = true; + } + + // Check for an explicit quality + if (options.quality) { + this._explicitQuality = true; + } + + options = L.setOptions(this, options); + this._infoPromise = null; + this._infoUrl = url; + this._baseUrl = this._templateUrl(); + this._getInfo(); + }, + getTileUrl: function(coords) { + var _this = this, + x = coords.x, + y = (coords.y), + zoom = _this._getZoomForUrl(), + scale = Math.pow(2, _this.maxNativeZoom - zoom), + tileBaseSize = _this.options.tileSize * scale, + minx = (x * tileBaseSize), + miny = (y * tileBaseSize), + maxx = Math.min(minx + tileBaseSize, _this.x), + maxy = Math.min(miny + tileBaseSize, _this.y); + + var xDiff = (maxx - minx); + var yDiff = (maxy - miny); + + // Canonical URI Syntax for v2 + var size = Math.ceil(xDiff / scale) + ','; + if (_this.type === 'ImageService3') { + // Cannonical URI Syntax for v3 + size = size + Math.ceil(yDiff / scale); + } + + return L.Util.template(this._baseUrl, L.extend({ + format: _this.options.tileFormat, + quality: _this.quality, + region: [minx, miny, xDiff, yDiff].join(','), + rotation: 0, + size: size + }, this.options)); + }, + onAdd: function(map) { + var _this = this; + + // Wait for info.json fetch and parse to complete + Promise.all([_this._infoPromise]).then(function() { + // Store unmutated imageSizes + _this._imageSizesOriginal = _this._imageSizes.slice(0); + + // Set maxZoom for map + map._layersMaxZoom = _this.maxZoom; + + // Call add TileLayer + L.TileLayer.prototype.onAdd.call(_this, map); + + // Set minZoom and minNativeZoom based on how the imageSizes match up + var smallestImage = _this._imageSizes[0]; + var mapSize = _this._map.getSize(); + var newMinZoom = 0; + // Loop back through 5 times to see if a better fit can be found. + for (var i = 1; i <= 5; i++) { + if (smallestImage.x > mapSize.x || smallestImage.y > mapSize.y) { + smallestImage = smallestImage.divideBy(2); + _this._imageSizes.unshift(smallestImage); + newMinZoom = -i; + } else { + break; + } + } + _this.options.minZoom = newMinZoom; + _this.options.minNativeZoom = newMinZoom; + _this._prev_map_layersMinZoom = _this._map._layersMinZoom; + _this._map._layersMinZoom = newMinZoom; + + if (_this.options.fitBounds) { + _this._fitBounds(); + } + + if(_this.options.setMaxBounds) { + _this._setMaxBounds(); + } + + // Reset tile sizes to handle non 256x256 IIIF tiles + _this.on('tileload', function(tile, url) { + + var height = tile.tile.naturalHeight, + width = tile.tile.naturalWidth; + + // No need to resize if tile is 256 x 256 + if (height === 256 && width === 256) return; + + tile.tile.style.width = width + 'px'; + tile.tile.style.height = height + 'px'; + + }); + }) + .catch(function(err){ + console.error(err); + }); + }, + onRemove: function(map) { + var _this = this; + + map._layersMinZoom = _this._prev_map_layersMinZoom; + _this._imageSizes = _this._imageSizesOriginal; + + // Remove maxBounds set for this image + if(_this.options.setMaxBounds) { + map.setMaxBounds(null); + } + + // Call remove TileLayer + L.TileLayer.prototype.onRemove.call(_this, map); + + }, + _fitBounds: function() { + var _this = this; + + // Find best zoom level and center map + var initialZoom = _this._getInitialZoom(_this._map.getSize()); + var offset = _this._imageSizes.length - 1 - _this.options.maxNativeZoom; + var imageSize = _this._imageSizes[initialZoom + offset]; + var sw = _this._map.options.crs.pointToLatLng(L.point(0, imageSize.y), initialZoom); + var ne = _this._map.options.crs.pointToLatLng(L.point(imageSize.x, 0), initialZoom); + var bounds = L.latLngBounds(sw, ne); + + _this._map.fitBounds(bounds, true); + }, + _setMaxBounds: function() { + var _this = this; + + // Find best zoom level, center map, and constrain viewer + var initialZoom = _this._getInitialZoom(_this._map.getSize()); + var imageSize = _this._imageSizes[initialZoom]; + var sw = _this._map.options.crs.pointToLatLng(L.point(0, imageSize.y), initialZoom); + var ne = _this._map.options.crs.pointToLatLng(L.point(imageSize.x, 0), initialZoom); + var bounds = L.latLngBounds(sw, ne); + + _this._map.setMaxBounds(bounds, true); + }, + _getInfo: function() { + var _this = this; + + _this._infoPromise = fetch(_this._infoUrl) + .then(function(response) { + return response.json(); + }) + .catch(function(err){ + console.error(err); + }) + .then(function(data) { + _this.y = data.height; + _this.x = data.width; + + var tierSizes = [], + imageSizes = [], + scale, + width_, + height_, + tilesX_, + tilesY_; + + // Set quality based off of IIIF version + if (data.profile instanceof Array) { + _this.profile = data.profile[0]; + }else { + _this.profile = data.profile; + } + _this.type = data.type; + + _this._setQuality(); + + // Unless an explicit tileSize is set, use a preferred tileSize + if (!_this._explicitTileSize) { + // Set the default first + _this.options.tileSize = 256; + if (data.tiles) { + // Image API 2.0 Case + _this.options.tileSize = data.tiles[0].width; + } else if (data.tile_width){ + // Image API 1.1 Case + _this.options.tileSize = data.tile_width; + } + } + + function ceilLog2(x) { + return Math.ceil(Math.log(x) / Math.LN2); + } + // Calculates maximum native zoom for the layer + _this.maxNativeZoom = Math.max( + ceilLog2(_this.x / _this.options.tileSize), + ceilLog2(_this.y / _this.options.tileSize), + 0 + ); + _this.options.maxNativeZoom = _this.maxNativeZoom; + + // Enable zooming further than native if maxZoom option supplied + if (_this._customMaxZoom && _this.options.maxZoom > _this.maxNativeZoom) { + _this.maxZoom = _this.options.maxZoom; + } + else { + _this.maxZoom = _this.maxNativeZoom; + } + + for (var i = 0; i <= _this.maxZoom; i++) { + scale = Math.pow(2, _this.maxNativeZoom - i); + width_ = Math.ceil(_this.x / scale); + height_ = Math.ceil(_this.y / scale); + tilesX_ = Math.ceil(width_ / _this.options.tileSize); + tilesY_ = Math.ceil(height_ / _this.options.tileSize); + tierSizes.push([tilesX_, tilesY_]); + imageSizes.push(L.point(width_,height_)); + } + + _this._tierSizes = tierSizes; + _this._imageSizes = imageSizes; + }) + .catch(function(err){ + console.error(err); + }); + + }, + + _setQuality: function() { + var _this = this; + var profileToCheck = _this.profile; + + if (_this._explicitQuality) { + return; + } + + // If profile is an object + if (typeof(profileToCheck) === 'object') { + profileToCheck = profileToCheck['@id']; + } + + // Set the quality based on the IIIF compliance level + switch (true) { + case /^http:\/\/library.stanford.edu\/iiif\/image-api\/1.1\/compliance.html.*$/.test(profileToCheck): + _this.options.quality = 'native'; + break; + // Assume later profiles and set to default + default: + _this.options.quality = 'default'; + break; + } + }, + + _infoToBaseUrl: function() { + return this._infoUrl.replace('info.json', ''); + }, + _templateUrl: function() { + return this._infoToBaseUrl() + '{region}/{size}/{rotation}/{quality}.{format}'; + }, + _isValidTile: function(coords) { + var _this = this; + var zoom = _this._getZoomForUrl(); + var sizes = _this._tierSizes[zoom]; + var x = coords.x; + var y = coords.y; + if (zoom < 0 && x >= 0 && y >= 0) { + return true; + } + + if (!sizes) return false; + if (x < 0 || sizes[0] <= x || y < 0 || sizes[1] <= y) { + return false; + }else { + return true; + } + }, + _tileShouldBeLoaded: function(coords) { + return this._isValidTile(coords); + }, + _getInitialZoom: function (mapSize) { + var _this = this; + var tolerance = 0.8; + var imageSize; + // Calculate an offset between the zoom levels and the array accessors + var offset = _this._imageSizes.length - 1 - _this.options.maxNativeZoom; + for (var i = _this._imageSizes.length - 1; i >= 0; i--) { + imageSize = _this._imageSizes[i]; + if (imageSize.x * tolerance < mapSize.x && imageSize.y * tolerance < mapSize.y) { + return i - offset; + } + } + // return a default zoom + return 2; } + }); + + L.tileLayer.iiif = function(url, options) { + return new L.TileLayer.Iiif(url, options); }; -}(); -Blacklight.onLoad(function() { - Spotlight.activate(); -}); + (function (factory, window) { + /*globals define, module, require*/ -Spotlight.onLoad(function(){ - SpotlightNestable.init(); -}); + // define an AMD module that relies on 'leaflet' + if (typeof define === 'function' && define.amd) { + define(['leaflet'], factory); + + // define a Common JS module that relies on 'leaflet' + } else if (typeof exports === 'object') { + module.exports = factory(require('leaflet')); + } + + // attach your plugin to the global 'L' variable + if(typeof window !== 'undefined' && window.L){ + factory(window.L); + } + + }(function (L) { + // 🍂miniclass CancelableEvent (Event objects) + // 🍂method cancel() + // Cancel any subsequent action. + + // 🍂miniclass VertexEvent (Event objects) + // 🍂property vertex: VertexMarker + // The vertex that fires the event. + + // 🍂miniclass ShapeEvent (Event objects) + // 🍂property shape: Array + // The shape (LatLngs array) subject of the action. + + // 🍂miniclass CancelableVertexEvent (Event objects) + // 🍂inherits VertexEvent + // 🍂inherits CancelableEvent + + // 🍂miniclass CancelableShapeEvent (Event objects) + // 🍂inherits ShapeEvent + // 🍂inherits CancelableEvent + + // 🍂miniclass LayerEvent (Event objects) + // 🍂property layer: object + // The Layer (Marker, Polyline…) subject of the action. + + // 🍂namespace Editable; 🍂class Editable; 🍂aka L.Editable + // Main edition handler. By default, it is attached to the map + // as `map.editTools` property. + // Leaflet.Editable is made to be fully extendable. You have three ways to customize + // the behaviour: using options, listening to events, or extending. + L.Editable = L.Evented.extend({ + + statics: { + FORWARD: 1, + BACKWARD: -1 + }, + + options: { + + // You can pass them when creating a map using the `editOptions` key. + // 🍂option zIndex: int = 1000 + // The default zIndex of the editing tools. + zIndex: 1000, + + // 🍂option polygonClass: class = L.Polygon + // Class to be used when creating a new Polygon. + polygonClass: L.Polygon, + + // 🍂option polylineClass: class = L.Polyline + // Class to be used when creating a new Polyline. + polylineClass: L.Polyline, + + // 🍂option markerClass: class = L.Marker + // Class to be used when creating a new Marker. + markerClass: L.Marker, + + // 🍂option rectangleClass: class = L.Rectangle + // Class to be used when creating a new Rectangle. + rectangleClass: L.Rectangle, + + // 🍂option circleClass: class = L.Circle + // Class to be used when creating a new Circle. + circleClass: L.Circle, + + // 🍂option drawingCSSClass: string = 'leaflet-editable-drawing' + // CSS class to be added to the map container while drawing. + drawingCSSClass: 'leaflet-editable-drawing', + + // 🍂option drawingCursor: const = 'crosshair' + // Cursor mode set to the map while drawing. + drawingCursor: 'crosshair', + + // 🍂option editLayer: Layer = new L.LayerGroup() + // Layer used to store edit tools (vertex, line guide…). + editLayer: undefined, + + // 🍂option featuresLayer: Layer = new L.LayerGroup() + // Default layer used to store drawn features (Marker, Polyline…). + featuresLayer: undefined, + + // 🍂option polylineEditorClass: class = PolylineEditor + // Class to be used as Polyline editor. + polylineEditorClass: undefined, + + // 🍂option polygonEditorClass: class = PolygonEditor + // Class to be used as Polygon editor. + polygonEditorClass: undefined, + + // 🍂option markerEditorClass: class = MarkerEditor + // Class to be used as Marker editor. + markerEditorClass: undefined, + + // 🍂option rectangleEditorClass: class = RectangleEditor + // Class to be used as Rectangle editor. + rectangleEditorClass: undefined, + + // 🍂option circleEditorClass: class = CircleEditor + // Class to be used as Circle editor. + circleEditorClass: undefined, + + // 🍂option lineGuideOptions: hash = {} + // Options to be passed to the line guides. + lineGuideOptions: {}, + + // 🍂option skipMiddleMarkers: boolean = false + // Set this to true if you don't want middle markers. + skipMiddleMarkers: false + + }, + + initialize: function (map, options) { + L.setOptions(this, options); + this._lastZIndex = this.options.zIndex; + this.map = map; + this.editLayer = this.createEditLayer(); + this.featuresLayer = this.createFeaturesLayer(); + this.forwardLineGuide = this.createLineGuide(); + this.backwardLineGuide = this.createLineGuide(); + }, + + fireAndForward: function (type, e) { + e = e || {}; + e.editTools = this; + this.fire(type, e); + this.map.fire(type, e); + }, + + createLineGuide: function () { + var options = L.extend({dashArray: '5,10', weight: 1, interactive: false}, this.options.lineGuideOptions); + return L.polyline([], options); + }, + + createVertexIcon: function (options) { + return L.Browser.touch ? new L.Editable.TouchVertexIcon(options) : new L.Editable.VertexIcon(options); + }, + + createEditLayer: function () { + return this.options.editLayer || new L.LayerGroup().addTo(this.map); + }, + + createFeaturesLayer: function () { + return this.options.featuresLayer || new L.LayerGroup().addTo(this.map); + }, + + moveForwardLineGuide: function (latlng) { + if (this.forwardLineGuide._latlngs.length) { + this.forwardLineGuide._latlngs[1] = latlng; + this.forwardLineGuide._bounds.extend(latlng); + this.forwardLineGuide.redraw(); + } + }, + + moveBackwardLineGuide: function (latlng) { + if (this.backwardLineGuide._latlngs.length) { + this.backwardLineGuide._latlngs[1] = latlng; + this.backwardLineGuide._bounds.extend(latlng); + this.backwardLineGuide.redraw(); + } + }, + + anchorForwardLineGuide: function (latlng) { + this.forwardLineGuide._latlngs[0] = latlng; + this.forwardLineGuide._bounds.extend(latlng); + this.forwardLineGuide.redraw(); + }, + + anchorBackwardLineGuide: function (latlng) { + this.backwardLineGuide._latlngs[0] = latlng; + this.backwardLineGuide._bounds.extend(latlng); + this.backwardLineGuide.redraw(); + }, + + attachForwardLineGuide: function () { + this.editLayer.addLayer(this.forwardLineGuide); + }, + + attachBackwardLineGuide: function () { + this.editLayer.addLayer(this.backwardLineGuide); + }, + + detachForwardLineGuide: function () { + this.forwardLineGuide.setLatLngs([]); + this.editLayer.removeLayer(this.forwardLineGuide); + }, + + detachBackwardLineGuide: function () { + this.backwardLineGuide.setLatLngs([]); + this.editLayer.removeLayer(this.backwardLineGuide); + }, + + blockEvents: function () { + // Hack: force map not to listen to other layers events while drawing. + if (!this._oldTargets) { + this._oldTargets = this.map._targets; + this.map._targets = {}; + } + }, + + unblockEvents: function () { + if (this._oldTargets) { + // Reset, but keep targets created while drawing. + this.map._targets = L.extend(this.map._targets, this._oldTargets); + delete this._oldTargets; + } + }, + + registerForDrawing: function (editor) { + if (this._drawingEditor) this.unregisterForDrawing(this._drawingEditor); + this.blockEvents(); + editor.reset(); // Make sure editor tools still receive events. + this._drawingEditor = editor; + this.map.on('mousemove touchmove', editor.onDrawingMouseMove, editor); + this.map.on('mousedown', this.onMousedown, this); + this.map.on('mouseup', this.onMouseup, this); + L.DomUtil.addClass(this.map._container, this.options.drawingCSSClass); + this.defaultMapCursor = this.map._container.style.cursor; + this.map._container.style.cursor = this.options.drawingCursor; + }, + + unregisterForDrawing: function (editor) { + this.unblockEvents(); + L.DomUtil.removeClass(this.map._container, this.options.drawingCSSClass); + this.map._container.style.cursor = this.defaultMapCursor; + editor = editor || this._drawingEditor; + if (!editor) return; + this.map.off('mousemove touchmove', editor.onDrawingMouseMove, editor); + this.map.off('mousedown', this.onMousedown, this); + this.map.off('mouseup', this.onMouseup, this); + if (editor !== this._drawingEditor) return; + delete this._drawingEditor; + if (editor._drawing) editor.cancelDrawing(); + }, + + onMousedown: function (e) { + this._mouseDown = e; + this._drawingEditor.onDrawingMouseDown(e); + }, + + onMouseup: function (e) { + if (this._mouseDown) { + var editor = this._drawingEditor, + mouseDown = this._mouseDown; + this._mouseDown = null; + editor.onDrawingMouseUp(e); + if (this._drawingEditor !== editor) return; // onDrawingMouseUp may call unregisterFromDrawing. + var origin = L.point(mouseDown.originalEvent.clientX, mouseDown.originalEvent.clientY); + var distance = L.point(e.originalEvent.clientX, e.originalEvent.clientY).distanceTo(origin); + if (Math.abs(distance) < 9 * (window.devicePixelRatio || 1)) this._drawingEditor.onDrawingClick(e); + } + }, + + // 🍂section Public methods + // You will generally access them by the `map.editTools` + // instance: + // + // `map.editTools.startPolyline();` + + // 🍂method drawing(): boolean + // Return true if any drawing action is ongoing. + drawing: function () { + return this._drawingEditor && this._drawingEditor.drawing(); + }, + + // 🍂method stopDrawing() + // When you need to stop any ongoing drawing, without needing to know which editor is active. + stopDrawing: function () { + this.unregisterForDrawing(); + }, + + // 🍂method commitDrawing() + // When you need to commit any ongoing drawing, without needing to know which editor is active. + commitDrawing: function (e) { + if (!this._drawingEditor) return; + this._drawingEditor.commitDrawing(e); + }, + + connectCreatedToMap: function (layer) { + return this.featuresLayer.addLayer(layer); + }, + + // 🍂method startPolyline(latlng: L.LatLng, options: hash): L.Polyline + // Start drawing a Polyline. If `latlng` is given, a first point will be added. In any case, continuing on user click. + // If `options` is given, it will be passed to the Polyline class constructor. + startPolyline: function (latlng, options) { + var line = this.createPolyline([], options); + line.enableEdit(this.map).newShape(latlng); + return line; + }, + + // 🍂method startPolygon(latlng: L.LatLng, options: hash): L.Polygon + // Start drawing a Polygon. If `latlng` is given, a first point will be added. In any case, continuing on user click. + // If `options` is given, it will be passed to the Polygon class constructor. + startPolygon: function (latlng, options) { + var polygon = this.createPolygon([], options); + polygon.enableEdit(this.map).newShape(latlng); + return polygon; + }, + + // 🍂method startMarker(latlng: L.LatLng, options: hash): L.Marker + // Start adding a Marker. If `latlng` is given, the Marker will be shown first at this point. + // In any case, it will follow the user mouse, and will have a final `latlng` on next click (or touch). + // If `options` is given, it will be passed to the Marker class constructor. + startMarker: function (latlng, options) { + latlng = latlng || this.map.getCenter().clone(); + var marker = this.createMarker(latlng, options); + marker.enableEdit(this.map).startDrawing(); + return marker; + }, + + // 🍂method startRectangle(latlng: L.LatLng, options: hash): L.Rectangle + // Start drawing a Rectangle. If `latlng` is given, the Rectangle anchor will be added. In any case, continuing on user drag. + // If `options` is given, it will be passed to the Rectangle class constructor. + startRectangle: function(latlng, options) { + var corner = latlng || L.latLng([0, 0]); + var bounds = new L.LatLngBounds(corner, corner); + var rectangle = this.createRectangle(bounds, options); + rectangle.enableEdit(this.map).startDrawing(); + return rectangle; + }, + + // 🍂method startCircle(latlng: L.LatLng, options: hash): L.Circle + // Start drawing a Circle. If `latlng` is given, the Circle anchor will be added. In any case, continuing on user drag. + // If `options` is given, it will be passed to the Circle class constructor. + startCircle: function (latlng, options) { + latlng = latlng || this.map.getCenter().clone(); + var circle = this.createCircle(latlng, options); + circle.enableEdit(this.map).startDrawing(); + return circle; + }, + + startHole: function (editor, latlng) { + editor.newHole(latlng); + }, + + createLayer: function (klass, latlngs, options) { + options = L.Util.extend({editOptions: {editTools: this}}, options); + var layer = new klass(latlngs, options); + // 🍂namespace Editable + // 🍂event editable:created: LayerEvent + // Fired when a new feature (Marker, Polyline…) is created. + this.fireAndForward('editable:created', {layer: layer}); + return layer; + }, + + createPolyline: function (latlngs, options) { + return this.createLayer(options && options.polylineClass || this.options.polylineClass, latlngs, options); + }, + + createPolygon: function (latlngs, options) { + return this.createLayer(options && options.polygonClass || this.options.polygonClass, latlngs, options); + }, + + createMarker: function (latlng, options) { + return this.createLayer(options && options.markerClass || this.options.markerClass, latlng, options); + }, + + createRectangle: function (bounds, options) { + return this.createLayer(options && options.rectangleClass || this.options.rectangleClass, bounds, options); + }, + + createCircle: function (latlng, options) { + return this.createLayer(options && options.circleClass || this.options.circleClass, latlng, options); + } + + }); + + L.extend(L.Editable, { + + makeCancellable: function (e) { + e.cancel = function () { + e._cancelled = true; + }; + } + + }); + + // 🍂namespace Map; 🍂class Map + // Leaflet.Editable add options and events to the `L.Map` object. + // See `Editable` events for the list of events fired on the Map. + // 🍂example + // + // ```js + // var map = L.map('map', { + // editable: true, + // editOptions: { + // … + // } + // }); + // ``` + // 🍂section Editable Map Options + L.Map.mergeOptions({ + + // 🍂namespace Map + // 🍂section Map Options + // 🍂option editToolsClass: class = L.Editable + // Class to be used as vertex, for path editing. + editToolsClass: L.Editable, + + // 🍂option editable: boolean = false + // Whether to create a L.Editable instance at map init. + editable: false, + + // 🍂option editOptions: hash = {} + // Options to pass to L.Editable when instanciating. + editOptions: {} + + }); + + L.Map.addInitHook(function () { + + this.whenReady(function () { + if (this.options.editable) { + this.editTools = new this.options.editToolsClass(this, this.options.editOptions); + } + }); + + }); + + L.Editable.VertexIcon = L.DivIcon.extend({ + + options: { + iconSize: new L.Point(8, 8) + } + + }); + + L.Editable.TouchVertexIcon = L.Editable.VertexIcon.extend({ + + options: { + iconSize: new L.Point(20, 20) + } + + }); + + + // 🍂namespace Editable; 🍂class VertexMarker; Handler for dragging path vertices. + L.Editable.VertexMarker = L.Marker.extend({ + + options: { + draggable: true, + className: 'leaflet-div-icon leaflet-vertex-icon' + }, + + + // 🍂section Public methods + // The marker used to handle path vertex. You will usually interact with a `VertexMarker` + // instance when listening for events like `editable:vertex:ctrlclick`. + + initialize: function (latlng, latlngs, editor, options) { + // We don't use this._latlng, because on drag Leaflet replace it while + // we want to keep reference. + this.latlng = latlng; + this.latlngs = latlngs; + this.editor = editor; + L.Marker.prototype.initialize.call(this, latlng, options); + this.options.icon = this.editor.tools.createVertexIcon({className: this.options.className}); + this.latlng.__vertex = this; + this.editor.editLayer.addLayer(this); + this.setZIndexOffset(editor.tools._lastZIndex + 1); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + this.on('drag', this.onDrag); + this.on('dragstart', this.onDragStart); + this.on('dragend', this.onDragEnd); + this.on('mouseup', this.onMouseup); + this.on('click', this.onClick); + this.on('contextmenu', this.onContextMenu); + this.on('mousedown touchstart', this.onMouseDown); + this.addMiddleMarkers(); + }, + + onRemove: function (map) { + if (this.middleMarker) this.middleMarker.delete(); + delete this.latlng.__vertex; + this.off('drag', this.onDrag); + this.off('dragstart', this.onDragStart); + this.off('dragend', this.onDragEnd); + this.off('mouseup', this.onMouseup); + this.off('click', this.onClick); + this.off('contextmenu', this.onContextMenu); + this.off('mousedown touchstart', this.onMouseDown); + L.Marker.prototype.onRemove.call(this, map); + }, + + onDrag: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDrag(e); + var iconPos = L.DomUtil.getPosition(this._icon), + latlng = this._map.layerPointToLatLng(iconPos); + this.latlng.update(latlng); + this._latlng = this.latlng; // Push back to Leaflet our reference. + this.editor.refresh(); + if (this.middleMarker) this.middleMarker.updateLatLng(); + var next = this.getNext(); + if (next && next.middleMarker) next.middleMarker.updateLatLng(); + }, + + onDragStart: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDragStart(e); + }, + + onDragEnd: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDragEnd(e); + }, + + onClick: function (e) { + e.vertex = this; + this.editor.onVertexMarkerClick(e); + }, + + onMouseup: function (e) { + L.DomEvent.stop(e); + e.vertex = this; + this.editor.map.fire('mouseup', e); + }, + + onContextMenu: function (e) { + e.vertex = this; + this.editor.onVertexMarkerContextMenu(e); + }, + + onMouseDown: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseDown(e); + }, + + // 🍂method delete() + // Delete a vertex and the related LatLng. + delete: function () { + var next = this.getNext(); // Compute before changing latlng + this.latlngs.splice(this.getIndex(), 1); + this.editor.editLayer.removeLayer(this); + this.editor.onVertexDeleted({latlng: this.latlng, vertex: this}); + if (!this.latlngs.length) this.editor.deleteShape(this.latlngs); + if (next) next.resetMiddleMarker(); + this.editor.refresh(); + }, + + // 🍂method getIndex(): int + // Get the index of the current vertex among others of the same LatLngs group. + getIndex: function () { + return this.latlngs.indexOf(this.latlng); + }, + + // 🍂method getLastIndex(): int + // Get last vertex index of the LatLngs group of the current vertex. + getLastIndex: function () { + return this.latlngs.length - 1; + }, + + // 🍂method getPrevious(): VertexMarker + // Get the previous VertexMarker in the same LatLngs group. + getPrevious: function () { + if (this.latlngs.length < 2) return; + var index = this.getIndex(), + previousIndex = index - 1; + if (index === 0 && this.editor.CLOSED) previousIndex = this.getLastIndex(); + var previous = this.latlngs[previousIndex]; + if (previous) return previous.__vertex; + }, + + // 🍂method getNext(): VertexMarker + // Get the next VertexMarker in the same LatLngs group. + getNext: function () { + if (this.latlngs.length < 2) return; + var index = this.getIndex(), + nextIndex = index + 1; + if (index === this.getLastIndex() && this.editor.CLOSED) nextIndex = 0; + var next = this.latlngs[nextIndex]; + if (next) return next.__vertex; + }, + + addMiddleMarker: function (previous) { + if (!this.editor.hasMiddleMarkers()) return; + previous = previous || this.getPrevious(); + if (previous && !this.middleMarker) this.middleMarker = this.editor.addMiddleMarker(previous, this, this.latlngs, this.editor); + }, + + addMiddleMarkers: function () { + if (!this.editor.hasMiddleMarkers()) return; + var previous = this.getPrevious(); + if (previous) this.addMiddleMarker(previous); + var next = this.getNext(); + if (next) next.resetMiddleMarker(); + }, + + resetMiddleMarker: function () { + if (this.middleMarker) this.middleMarker.delete(); + this.addMiddleMarker(); + }, + + // 🍂method split() + // Split the vertex LatLngs group at its index, if possible. + split: function () { + if (!this.editor.splitShape) return; // Only for PolylineEditor + this.editor.splitShape(this.latlngs, this.getIndex()); + }, + + // 🍂method continue() + // Continue the vertex LatLngs from this vertex. Only active for first and last vertices of a Polyline. + continue: function () { + if (!this.editor.continueBackward) return; // Only for PolylineEditor + var index = this.getIndex(); + if (index === 0) this.editor.continueBackward(this.latlngs); + else if (index === this.getLastIndex()) this.editor.continueForward(this.latlngs); + } + + }); + + L.Editable.mergeOptions({ + + // 🍂namespace Editable + // 🍂option vertexMarkerClass: class = VertexMarker + // Class to be used as vertex, for path editing. + vertexMarkerClass: L.Editable.VertexMarker + + }); + + L.Editable.MiddleMarker = L.Marker.extend({ + + options: { + opacity: 0.5, + className: 'leaflet-div-icon leaflet-middle-icon', + draggable: true + }, + + initialize: function (left, right, latlngs, editor, options) { + this.left = left; + this.right = right; + this.editor = editor; + this.latlngs = latlngs; + L.Marker.prototype.initialize.call(this, this.computeLatLng(), options); + this._opacity = this.options.opacity; + this.options.icon = this.editor.tools.createVertexIcon({className: this.options.className}); + this.editor.editLayer.addLayer(this); + this.setVisibility(); + }, + + setVisibility: function () { + var leftPoint = this._map.latLngToContainerPoint(this.left.latlng), + rightPoint = this._map.latLngToContainerPoint(this.right.latlng), + size = L.point(this.options.icon.options.iconSize); + if (leftPoint.distanceTo(rightPoint) < size.x * 3) this.hide(); + else this.show(); + }, + + show: function () { + this.setOpacity(this._opacity); + }, + + hide: function () { + this.setOpacity(0); + }, + + updateLatLng: function () { + this.setLatLng(this.computeLatLng()); + this.setVisibility(); + }, + + computeLatLng: function () { + var leftPoint = this.editor.map.latLngToContainerPoint(this.left.latlng), + rightPoint = this.editor.map.latLngToContainerPoint(this.right.latlng), + y = (leftPoint.y + rightPoint.y) / 2, + x = (leftPoint.x + rightPoint.x) / 2; + return this.editor.map.containerPointToLatLng([x, y]); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + L.DomEvent.on(this._icon, 'mousedown touchstart', this.onMouseDown, this); + map.on('zoomend', this.setVisibility, this); + }, + + onRemove: function (map) { + delete this.right.middleMarker; + L.DomEvent.off(this._icon, 'mousedown touchstart', this.onMouseDown, this); + map.off('zoomend', this.setVisibility, this); + L.Marker.prototype.onRemove.call(this, map); + }, + + onMouseDown: function (e) { + var iconPos = L.DomUtil.getPosition(this._icon), + latlng = this.editor.map.layerPointToLatLng(iconPos); + e = { + originalEvent: e, + latlng: latlng + }; + if (this.options.opacity === 0) return; + L.Editable.makeCancellable(e); + this.editor.onMiddleMarkerMouseDown(e); + if (e._cancelled) return; + this.latlngs.splice(this.index(), 0, e.latlng); + this.editor.refresh(); + var icon = this._icon; + var marker = this.editor.addVertexMarker(e.latlng, this.latlngs); + this.editor.onNewVertex(marker); + /* Hack to workaround browser not firing touchend when element is no more on DOM */ + var parent = marker._icon.parentNode; + parent.removeChild(marker._icon); + marker._icon = icon; + parent.appendChild(marker._icon); + marker._initIcon(); + marker._initInteraction(); + marker.setOpacity(1); + /* End hack */ + // Transfer ongoing dragging to real marker + L.Draggable._dragging = false; + marker.dragging._draggable._onDown(e.originalEvent); + this.delete(); + }, + + delete: function () { + this.editor.editLayer.removeLayer(this); + }, + + index: function () { + return this.latlngs.indexOf(this.right.latlng); + } + + }); + + L.Editable.mergeOptions({ + + // 🍂namespace Editable + // 🍂option middleMarkerClass: class = VertexMarker + // Class to be used as middle vertex, pulled by the user to create a new point in the middle of a path. + middleMarkerClass: L.Editable.MiddleMarker + + }); + + // 🍂namespace Editable; 🍂class BaseEditor; 🍂aka L.Editable.BaseEditor + // When editing a feature (Marker, Polyline…), an editor is attached to it. This + // editor basically knows how to handle the edition. + L.Editable.BaseEditor = L.Handler.extend({ + + initialize: function (map, feature, options) { + L.setOptions(this, options); + this.map = map; + this.feature = feature; + this.feature.editor = this; + this.editLayer = new L.LayerGroup(); + this.tools = this.options.editTools || map.editTools; + }, + + // 🍂method enable(): this + // Set up the drawing tools for the feature to be editable. + addHooks: function () { + if (this.isConnected()) this.onFeatureAdd(); + else this.feature.once('add', this.onFeatureAdd, this); + this.onEnable(); + this.feature.on(this._getEvents(), this); + return; + }, + + // 🍂method disable(): this + // Remove the drawing tools for the feature. + removeHooks: function () { + this.feature.off(this._getEvents(), this); + if (this.feature.dragging) this.feature.dragging.disable(); + this.editLayer.clearLayers(); + this.tools.editLayer.removeLayer(this.editLayer); + this.onDisable(); + if (this._drawing) this.cancelDrawing(); + return; + }, + + // 🍂method drawing(): boolean + // Return true if any drawing action is ongoing with this editor. + drawing: function () { + return !!this._drawing; + }, + + reset: function () {}, + + onFeatureAdd: function () { + this.tools.editLayer.addLayer(this.editLayer); + if (this.feature.dragging) this.feature.dragging.enable(); + }, + + hasMiddleMarkers: function () { + return !this.options.skipMiddleMarkers && !this.tools.options.skipMiddleMarkers; + }, + + fireAndForward: function (type, e) { + e = e || {}; + e.layer = this.feature; + this.feature.fire(type, e); + this.tools.fireAndForward(type, e); + }, + + onEnable: function () { + // 🍂namespace Editable + // 🍂event editable:enable: Event + // Fired when an existing feature is ready to be edited. + this.fireAndForward('editable:enable'); + }, + + onDisable: function () { + // 🍂namespace Editable + // 🍂event editable:disable: Event + // Fired when an existing feature is not ready anymore to be edited. + this.fireAndForward('editable:disable'); + }, + + onEditing: function () { + // 🍂namespace Editable + // 🍂event editable:editing: Event + // Fired as soon as any change is made to the feature geometry. + this.fireAndForward('editable:editing'); + }, + + onStartDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:start: Event + // Fired when a feature is to be drawn. + this.fireAndForward('editable:drawing:start'); + }, + + onEndDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:end: Event + // Fired when a feature is not drawn anymore. + this.fireAndForward('editable:drawing:end'); + }, + + onCancelDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:cancel: Event + // Fired when user cancel drawing while a feature is being drawn. + this.fireAndForward('editable:drawing:cancel'); + }, + + onCommitDrawing: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:commit: Event + // Fired when user finish drawing a feature. + this.fireAndForward('editable:drawing:commit', e); + }, + + onDrawingMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:mousedown: Event + // Fired when user `mousedown` while drawing. + this.fireAndForward('editable:drawing:mousedown', e); + }, + + onDrawingMouseUp: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:mouseup: Event + // Fired when user `mouseup` while drawing. + this.fireAndForward('editable:drawing:mouseup', e); + }, + + startDrawing: function () { + if (!this._drawing) this._drawing = L.Editable.FORWARD; + this.tools.registerForDrawing(this); + this.onStartDrawing(); + }, + + commitDrawing: function (e) { + this.onCommitDrawing(e); + this.endDrawing(); + }, + + cancelDrawing: function () { + // If called during a vertex drag, the vertex will be removed before + // the mouseup fires on it. This is a workaround. Maybe better fix is + // To have L.Draggable reset it's status on disable (Leaflet side). + L.Draggable._dragging = false; + this.onCancelDrawing(); + this.endDrawing(); + }, + + endDrawing: function () { + this._drawing = false; + this.tools.unregisterForDrawing(this); + this.onEndDrawing(); + }, + + onDrawingClick: function (e) { + if (!this.drawing()) return; + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:click: CancelableEvent + // Fired when user `click` while drawing, before any internal action is being processed. + this.fireAndForward('editable:drawing:click', e); + if (e._cancelled) return; + if (!this.isConnected()) this.connect(e); + this.processDrawingClick(e); + }, + + isConnected: function () { + return this.map.hasLayer(this.feature); + }, + + connect: function (e) { + this.tools.connectCreatedToMap(this.feature); + this.tools.editLayer.addLayer(this.editLayer); + }, + + onMove: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:move: Event + // Fired when `move` mouse while drawing, while dragging a marker, and while dragging a vertex. + this.fireAndForward('editable:drawing:move', e); + }, + + onDrawingMouseMove: function (e) { + this.onMove(e); + }, + + _getEvents: function () { + return { + dragstart: this.onDragStart, + drag: this.onDrag, + dragend: this.onDragEnd, + remove: this.disable + }; + }, + + onDragStart: function (e) { + this.onEditing(); + // 🍂namespace Editable + // 🍂event editable:dragstart: Event + // Fired before a path feature is dragged. + this.fireAndForward('editable:dragstart', e); + }, + + onDrag: function (e) { + this.onMove(e); + // 🍂namespace Editable + // 🍂event editable:drag: Event + // Fired when a path feature is being dragged. + this.fireAndForward('editable:drag', e); + }, + + onDragEnd: function (e) { + // 🍂namespace Editable + // 🍂event editable:dragend: Event + // Fired after a path feature has been dragged. + this.fireAndForward('editable:dragend', e); + } + + }); + + // 🍂namespace Editable; 🍂class MarkerEditor; 🍂aka L.Editable.MarkerEditor + // 🍂inherits BaseEditor + // Editor for Marker. + L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({ + + onDrawingMouseMove: function (e) { + L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e); + if (this._drawing) this.feature.setLatLng(e.latlng); + }, + + processDrawingClick: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:clicked: Event + // Fired when user `click` while drawing, after all internal actions. + this.fireAndForward('editable:drawing:clicked', e); + this.commitDrawing(e); + }, + + connect: function (e) { + // On touch, the latlng has not been updated because there is + // no mousemove. + if (e) this.feature._latlng = e.latlng; + L.Editable.BaseEditor.prototype.connect.call(this, e); + } + + }); + + // 🍂namespace Editable; 🍂class PathEditor; 🍂aka L.Editable.PathEditor + // 🍂inherits BaseEditor + // Base class for all path editors. + L.Editable.PathEditor = L.Editable.BaseEditor.extend({ + + CLOSED: false, + MIN_VERTEX: 2, + + addHooks: function () { + L.Editable.BaseEditor.prototype.addHooks.call(this); + if (this.feature) this.initVertexMarkers(); + return this; + }, + + initVertexMarkers: function (latlngs) { + if (!this.enabled()) return; + latlngs = latlngs || this.getLatLngs(); + if (isFlat(latlngs)) this.addVertexMarkers(latlngs); + else for (var i = 0; i < latlngs.length; i++) this.initVertexMarkers(latlngs[i]); + }, + + getLatLngs: function () { + return this.feature.getLatLngs(); + }, + + // 🍂method reset() + // Rebuild edit elements (Vertex, MiddleMarker, etc.). + reset: function () { + this.editLayer.clearLayers(); + this.initVertexMarkers(); + }, + + addVertexMarker: function (latlng, latlngs) { + return new this.tools.options.vertexMarkerClass(latlng, latlngs, this); + }, + + onNewVertex: function (vertex) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:new: VertexEvent + // Fired when a new vertex is created. + this.fireAndForward('editable:vertex:new', {latlng: vertex.latlng, vertex: vertex}); + }, + + addVertexMarkers: function (latlngs) { + for (var i = 0; i < latlngs.length; i++) { + this.addVertexMarker(latlngs[i], latlngs); + } + }, + + refreshVertexMarkers: function (latlngs) { + latlngs = latlngs || this.getDefaultLatLngs(); + for (var i = 0; i < latlngs.length; i++) { + latlngs[i].__vertex.update(); + } + }, + + addMiddleMarker: function (left, right, latlngs) { + return new this.tools.options.middleMarkerClass(left, right, latlngs, this); + }, + + onVertexMarkerClick: function (e) { + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:click: CancelableVertexEvent + // Fired when a `click` is issued on a vertex, before any internal action is being processed. + this.fireAndForward('editable:vertex:click', e); + if (e._cancelled) return; + if (this.tools.drawing() && this.tools._drawingEditor !== this) return; + var index = e.vertex.getIndex(), commit; + if (e.originalEvent.ctrlKey) { + this.onVertexMarkerCtrlClick(e); + } else if (e.originalEvent.altKey) { + this.onVertexMarkerAltClick(e); + } else if (e.originalEvent.shiftKey) { + this.onVertexMarkerShiftClick(e); + } else if (e.originalEvent.metaKey) { + this.onVertexMarkerMetaKeyClick(e); + } else if (index === e.vertex.getLastIndex() && this._drawing === L.Editable.FORWARD) { + if (index >= this.MIN_VERTEX - 1) commit = true; + } else if (index === 0 && this._drawing === L.Editable.BACKWARD && this._drawnLatLngs.length >= this.MIN_VERTEX) { + commit = true; + } else if (index === 0 && this._drawing === L.Editable.FORWARD && this._drawnLatLngs.length >= this.MIN_VERTEX && this.CLOSED) { + commit = true; // Allow to close on first point also for polygons + } else { + this.onVertexRawMarkerClick(e); + } + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:clicked: VertexEvent + // Fired when a `click` is issued on a vertex, after all internal actions. + this.fireAndForward('editable:vertex:clicked', e); + if (commit) this.commitDrawing(e); + }, + + onVertexRawMarkerClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:rawclick: CancelableVertexEvent + // Fired when a `click` is issued on a vertex without any special key and without being in drawing mode. + this.fireAndForward('editable:vertex:rawclick', e); + if (e._cancelled) return; + if (!this.vertexCanBeDeleted(e.vertex)) return; + e.vertex.delete(); + }, + + vertexCanBeDeleted: function (vertex) { + return vertex.latlngs.length > this.MIN_VERTEX; + }, + + onVertexDeleted: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:deleted: VertexEvent + // Fired after a vertex has been deleted by user. + this.fireAndForward('editable:vertex:deleted', e); + }, + + onVertexMarkerCtrlClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:ctrlclick: VertexEvent + // Fired when a `click` with `ctrlKey` is issued on a vertex. + this.fireAndForward('editable:vertex:ctrlclick', e); + }, + + onVertexMarkerShiftClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:shiftclick: VertexEvent + // Fired when a `click` with `shiftKey` is issued on a vertex. + this.fireAndForward('editable:vertex:shiftclick', e); + }, + + onVertexMarkerMetaKeyClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:metakeyclick: VertexEvent + // Fired when a `click` with `metaKey` is issued on a vertex. + this.fireAndForward('editable:vertex:metakeyclick', e); + }, + + onVertexMarkerAltClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:altclick: VertexEvent + // Fired when a `click` with `altKey` is issued on a vertex. + this.fireAndForward('editable:vertex:altclick', e); + }, + + onVertexMarkerContextMenu: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:contextmenu: VertexEvent + // Fired when a `contextmenu` is issued on a vertex. + this.fireAndForward('editable:vertex:contextmenu', e); + }, + + onVertexMarkerMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mousedown: VertexEvent + // Fired when user `mousedown` a vertex. + this.fireAndForward('editable:vertex:mousedown', e); + }, + + onMiddleMarkerMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section MiddleMarker events + // 🍂event editable:middlemarker:mousedown: VertexEvent + // Fired when user `mousedown` a middle marker. + this.fireAndForward('editable:middlemarker:mousedown', e); + }, + + onVertexMarkerDrag: function (e) { + this.onMove(e); + if (this.feature._bounds) this.extendBounds(e); + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:drag: VertexEvent + // Fired when a vertex is dragged by user. + this.fireAndForward('editable:vertex:drag', e); + }, + + onVertexMarkerDragStart: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:dragstart: VertexEvent + // Fired before a vertex is dragged by user. + this.fireAndForward('editable:vertex:dragstart', e); + }, + + onVertexMarkerDragEnd: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:dragend: VertexEvent + // Fired after a vertex is dragged by user. + this.fireAndForward('editable:vertex:dragend', e); + }, + + setDrawnLatLngs: function (latlngs) { + this._drawnLatLngs = latlngs || this.getDefaultLatLngs(); + }, + + startDrawing: function () { + if (!this._drawnLatLngs) this.setDrawnLatLngs(); + L.Editable.BaseEditor.prototype.startDrawing.call(this); + }, + + startDrawingForward: function () { + this.startDrawing(); + }, + + endDrawing: function () { + this.tools.detachForwardLineGuide(); + this.tools.detachBackwardLineGuide(); + if (this._drawnLatLngs && this._drawnLatLngs.length < this.MIN_VERTEX) this.deleteShape(this._drawnLatLngs); + L.Editable.BaseEditor.prototype.endDrawing.call(this); + delete this._drawnLatLngs; + }, + + addLatLng: function (latlng) { + if (this._drawing === L.Editable.FORWARD) this._drawnLatLngs.push(latlng); + else this._drawnLatLngs.unshift(latlng); + this.feature._bounds.extend(latlng); + var vertex = this.addVertexMarker(latlng, this._drawnLatLngs); + this.onNewVertex(vertex); + this.refresh(); + }, + + newPointForward: function (latlng) { + this.addLatLng(latlng); + this.tools.attachForwardLineGuide(); + this.tools.anchorForwardLineGuide(latlng); + }, + + newPointBackward: function (latlng) { + this.addLatLng(latlng); + this.tools.anchorBackwardLineGuide(latlng); + }, + + // 🍂namespace PathEditor + // 🍂method push() + // Programmatically add a point while drawing. + push: function (latlng) { + if (!latlng) return console.error('L.Editable.PathEditor.push expect a vaild latlng as parameter'); + if (this._drawing === L.Editable.FORWARD) this.newPointForward(latlng); + else this.newPointBackward(latlng); + }, + + removeLatLng: function (latlng) { + latlng.__vertex.delete(); + this.refresh(); + }, + + // 🍂method pop(): L.LatLng or null + // Programmatically remove last point (if any) while drawing. + pop: function () { + if (this._drawnLatLngs.length <= 1) return; + var latlng; + if (this._drawing === L.Editable.FORWARD) latlng = this._drawnLatLngs[this._drawnLatLngs.length - 1]; + else latlng = this._drawnLatLngs[0]; + this.removeLatLng(latlng); + if (this._drawing === L.Editable.FORWARD) this.tools.anchorForwardLineGuide(this._drawnLatLngs[this._drawnLatLngs.length - 1]); + else this.tools.anchorForwardLineGuide(this._drawnLatLngs[0]); + return latlng; + }, + + processDrawingClick: function (e) { + if (e.vertex && e.vertex.editor === this) return; + if (this._drawing === L.Editable.FORWARD) this.newPointForward(e.latlng); + else this.newPointBackward(e.latlng); + this.fireAndForward('editable:drawing:clicked', e); + }, + + onDrawingMouseMove: function (e) { + L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e); + if (this._drawing) { + this.tools.moveForwardLineGuide(e.latlng); + this.tools.moveBackwardLineGuide(e.latlng); + } + }, + + refresh: function () { + this.feature.redraw(); + this.onEditing(); + }, + + // 🍂namespace PathEditor + // 🍂method newShape(latlng?: L.LatLng) + // Add a new shape (Polyline, Polygon) in a multi, and setup up drawing tools to draw it; + // if optional `latlng` is given, start a path at this point. + newShape: function (latlng) { + var shape = this.addNewEmptyShape(); + if (!shape) return; + this.setDrawnLatLngs(shape[0] || shape); // Polygon or polyline + this.startDrawingForward(); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:new: ShapeEvent + // Fired when a new shape is created in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:new', {shape: shape}); + if (latlng) this.newPointForward(latlng); + }, + + deleteShape: function (shape, latlngs) { + var e = {shape: shape}; + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:delete: CancelableShapeEvent + // Fired before a new shape is deleted in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:delete', e); + if (e._cancelled) return; + shape = this._deleteShape(shape, latlngs); + if (this.ensureNotFlat) this.ensureNotFlat(); // Polygon. + this.feature.setLatLngs(this.getLatLngs()); // Force bounds reset. + this.refresh(); + this.reset(); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:deleted: ShapeEvent + // Fired after a new shape is deleted in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:deleted', {shape: shape}); + return shape; + }, + + _deleteShape: function (shape, latlngs) { + latlngs = latlngs || this.getLatLngs(); + if (!latlngs.length) return; + var self = this, + inplaceDelete = function (latlngs, shape) { + // Called when deleting a flat latlngs + shape = latlngs.splice(0, Number.MAX_VALUE); + return shape; + }, + spliceDelete = function (latlngs, shape) { + // Called when removing a latlngs inside an array + latlngs.splice(latlngs.indexOf(shape), 1); + if (!latlngs.length) self._deleteShape(latlngs); + return shape; + }; + if (latlngs === shape) return inplaceDelete(latlngs, shape); + for (var i = 0; i < latlngs.length; i++) { + if (latlngs[i] === shape) return spliceDelete(latlngs, shape); + else if (latlngs[i].indexOf(shape) !== -1) return spliceDelete(latlngs[i], shape); + } + }, + + // 🍂namespace PathEditor + // 🍂method deleteShapeAt(latlng: L.LatLng): Array + // Remove a path shape at the given `latlng`. + deleteShapeAt: function (latlng) { + var shape = this.feature.shapeAt(latlng); + if (shape) return this.deleteShape(shape); + }, + + // 🍂method appendShape(shape: Array) + // Append a new shape to the Polygon or Polyline. + appendShape: function (shape) { + this.insertShape(shape); + }, + + // 🍂method prependShape(shape: Array) + // Prepend a new shape to the Polygon or Polyline. + prependShape: function (shape) { + this.insertShape(shape, 0); + }, + + // 🍂method insertShape(shape: Array, index: int) + // Insert a new shape to the Polygon or Polyline at given index (default is to append). + insertShape: function (shape, index) { + this.ensureMulti(); + shape = this.formatShape(shape); + if (typeof index === 'undefined') index = this.feature._latlngs.length; + this.feature._latlngs.splice(index, 0, shape); + this.feature.redraw(); + if (this._enabled) this.reset(); + }, + + extendBounds: function (e) { + this.feature._bounds.extend(e.vertex.latlng); + }, + + onDragStart: function (e) { + this.editLayer.clearLayers(); + L.Editable.BaseEditor.prototype.onDragStart.call(this, e); + }, + + onDragEnd: function (e) { + this.initVertexMarkers(); + L.Editable.BaseEditor.prototype.onDragEnd.call(this, e); + } + + }); + + // 🍂namespace Editable; 🍂class PolylineEditor; 🍂aka L.Editable.PolylineEditor + // 🍂inherits PathEditor + L.Editable.PolylineEditor = L.Editable.PathEditor.extend({ + + startDrawingBackward: function () { + this._drawing = L.Editable.BACKWARD; + this.startDrawing(); + }, + + // 🍂method continueBackward(latlngs?: Array) + // Set up drawing tools to continue the line backward. + continueBackward: function (latlngs) { + if (this.drawing()) return; + latlngs = latlngs || this.getDefaultLatLngs(); + this.setDrawnLatLngs(latlngs); + if (latlngs.length > 0) { + this.tools.attachBackwardLineGuide(); + this.tools.anchorBackwardLineGuide(latlngs[0]); + } + this.startDrawingBackward(); + }, + + // 🍂method continueForward(latlngs?: Array) + // Set up drawing tools to continue the line forward. + continueForward: function (latlngs) { + if (this.drawing()) return; + latlngs = latlngs || this.getDefaultLatLngs(); + this.setDrawnLatLngs(latlngs); + if (latlngs.length > 0) { + this.tools.attachForwardLineGuide(); + this.tools.anchorForwardLineGuide(latlngs[latlngs.length - 1]); + } + this.startDrawingForward(); + }, + + getDefaultLatLngs: function (latlngs) { + latlngs = latlngs || this.feature._latlngs; + if (!latlngs.length || latlngs[0] instanceof L.LatLng) return latlngs; + else return this.getDefaultLatLngs(latlngs[0]); + }, + + ensureMulti: function () { + if (this.feature._latlngs.length && isFlat(this.feature._latlngs)) { + this.feature._latlngs = [this.feature._latlngs]; + } + }, + + addNewEmptyShape: function () { + if (this.feature._latlngs.length) { + var shape = []; + this.appendShape(shape); + return shape; + } else { + return this.feature._latlngs; + } + }, + + formatShape: function (shape) { + if (isFlat(shape)) return shape; + else if (shape[0]) return this.formatShape(shape[0]); + }, + + // 🍂method splitShape(latlngs?: Array, index: int) + // Split the given `latlngs` shape at index `index` and integrate new shape in instance `latlngs`. + splitShape: function (shape, index) { + if (!index || index >= shape.length - 1) return; + this.ensureMulti(); + var shapeIndex = this.feature._latlngs.indexOf(shape); + if (shapeIndex === -1) return; + var first = shape.slice(0, index + 1), + second = shape.slice(index); + // We deal with reference, we don't want twice the same latlng around. + second[0] = L.latLng(second[0].lat, second[0].lng, second[0].alt); + this.feature._latlngs.splice(shapeIndex, 1, first, second); + this.refresh(); + this.reset(); + } + + }); + + // 🍂namespace Editable; 🍂class PolygonEditor; 🍂aka L.Editable.PolygonEditor + // 🍂inherits PathEditor + L.Editable.PolygonEditor = L.Editable.PathEditor.extend({ + + CLOSED: true, + MIN_VERTEX: 3, + + newPointForward: function (latlng) { + L.Editable.PathEditor.prototype.newPointForward.call(this, latlng); + if (!this.tools.backwardLineGuide._latlngs.length) this.tools.anchorBackwardLineGuide(latlng); + if (this._drawnLatLngs.length === 2) this.tools.attachBackwardLineGuide(); + }, + + addNewEmptyHole: function (latlng) { + this.ensureNotFlat(); + var latlngs = this.feature.shapeAt(latlng); + if (!latlngs) return; + var holes = []; + latlngs.push(holes); + return holes; + }, + + // 🍂method newHole(latlng?: L.LatLng, index: int) + // Set up drawing tools for creating a new hole on the Polygon. If the `latlng` param is given, a first point is created. + newHole: function (latlng) { + var holes = this.addNewEmptyHole(latlng); + if (!holes) return; + this.setDrawnLatLngs(holes); + this.startDrawingForward(); + if (latlng) this.newPointForward(latlng); + }, + + addNewEmptyShape: function () { + if (this.feature._latlngs.length && this.feature._latlngs[0].length) { + var shape = []; + this.appendShape(shape); + return shape; + } else { + return this.feature._latlngs; + } + }, + + ensureMulti: function () { + if (this.feature._latlngs.length && isFlat(this.feature._latlngs[0])) { + this.feature._latlngs = [this.feature._latlngs]; + } + }, + + ensureNotFlat: function () { + if (!this.feature._latlngs.length || isFlat(this.feature._latlngs)) this.feature._latlngs = [this.feature._latlngs]; + }, + + vertexCanBeDeleted: function (vertex) { + var parent = this.feature.parentShape(vertex.latlngs), + idx = L.Util.indexOf(parent, vertex.latlngs); + if (idx > 0) return true; // Holes can be totally deleted without removing the layer itself. + return L.Editable.PathEditor.prototype.vertexCanBeDeleted.call(this, vertex); + }, + + getDefaultLatLngs: function () { + if (!this.feature._latlngs.length) this.feature._latlngs.push([]); + return this.feature._latlngs[0]; + }, + + formatShape: function (shape) { + // [[1, 2], [3, 4]] => must be nested + // [] => must be nested + // [[]] => is already nested + if (isFlat(shape) && (!shape[0] || shape[0].length !== 0)) return [shape]; + else return shape; + } + + }); + + // 🍂namespace Editable; 🍂class RectangleEditor; 🍂aka L.Editable.RectangleEditor + // 🍂inherits PathEditor + L.Editable.RectangleEditor = L.Editable.PathEditor.extend({ + + CLOSED: true, + MIN_VERTEX: 4, + + options: { + skipMiddleMarkers: true + }, + + extendBounds: function (e) { + var index = e.vertex.getIndex(), + next = e.vertex.getNext(), + previous = e.vertex.getPrevious(), + oppositeIndex = (index + 2) % 4, + opposite = e.vertex.latlngs[oppositeIndex], + bounds = new L.LatLngBounds(e.latlng, opposite); + // Update latlngs by hand to preserve order. + previous.latlng.update([e.latlng.lat, opposite.lng]); + next.latlng.update([opposite.lat, e.latlng.lng]); + this.updateBounds(bounds); + this.refreshVertexMarkers(); + }, + + onDrawingMouseDown: function (e) { + L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e); + this.connect(); + var latlngs = this.getDefaultLatLngs(); + // L.Polygon._convertLatLngs removes last latlng if it equals first point, + // which is the case here as all latlngs are [0, 0] + if (latlngs.length === 3) latlngs.push(e.latlng); + var bounds = new L.LatLngBounds(e.latlng, e.latlng); + this.updateBounds(bounds); + this.updateLatLngs(bounds); + this.refresh(); + this.reset(); + // Stop dragging map. + // L.Draggable has two workflows: + // - mousedown => mousemove => mouseup + // - touchstart => touchmove => touchend + // Problem: L.Map.Tap does not allow us to listen to touchstart, so we only + // can deal with mousedown, but then when in a touch device, we are dealing with + // simulated events (actually simulated by L.Map.Tap), which are no more taken + // into account by L.Draggable. + // Ref.: https://github.com/Leaflet/Leaflet.Editable/issues/103 + e.originalEvent._simulated = false; + this.map.dragging._draggable._onUp(e.originalEvent); + // Now transfer ongoing drag action to the bottom right corner. + // Should we refine which corne will handle the drag according to + // drag direction? + latlngs[3].__vertex.dragging._draggable._onDown(e.originalEvent); + }, + + onDrawingMouseUp: function (e) { + this.commitDrawing(e); + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e); + }, + + onDrawingMouseMove: function (e) { + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e); + }, + + + getDefaultLatLngs: function (latlngs) { + return latlngs || this.feature._latlngs[0]; + }, + + updateBounds: function (bounds) { + this.feature._bounds = bounds; + }, + + updateLatLngs: function (bounds) { + var latlngs = this.getDefaultLatLngs(), + newLatlngs = this.feature._boundsToLatLngs(bounds); + // Keep references. + for (var i = 0; i < latlngs.length; i++) { + latlngs[i].update(newLatlngs[i]); + } } + + }); + + // 🍂namespace Editable; 🍂class CircleEditor; 🍂aka L.Editable.CircleEditor + // 🍂inherits PathEditor + L.Editable.CircleEditor = L.Editable.PathEditor.extend({ + + MIN_VERTEX: 2, + + options: { + skipMiddleMarkers: true + }, + + initialize: function (map, feature, options) { + L.Editable.PathEditor.prototype.initialize.call(this, map, feature, options); + this._resizeLatLng = this.computeResizeLatLng(); + }, + + computeResizeLatLng: function () { + // While circle is not added to the map, _radius is not set. + var delta = (this.feature._radius || this.feature._mRadius) * Math.cos(Math.PI / 4), + point = this.map.project(this.feature._latlng); + return this.map.unproject([point.x + delta, point.y - delta]); + }, + + updateResizeLatLng: function () { + this._resizeLatLng.update(this.computeResizeLatLng()); + this._resizeLatLng.__vertex.update(); + }, + + getLatLngs: function () { + return [this.feature._latlng, this._resizeLatLng]; + }, + + getDefaultLatLngs: function () { + return this.getLatLngs(); + }, + + onVertexMarkerDrag: function (e) { + if (e.vertex.getIndex() === 1) this.resize(e); + else this.updateResizeLatLng(e); + L.Editable.PathEditor.prototype.onVertexMarkerDrag.call(this, e); + }, + + resize: function (e) { + var radius = this.feature._latlng.distanceTo(e.latlng); + this.feature.setRadius(radius); + }, + + onDrawingMouseDown: function (e) { + L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e); + this._resizeLatLng.update(e.latlng); + this.feature._latlng.update(e.latlng); + this.connect(); + // Stop dragging map. + e.originalEvent._simulated = false; + this.map.dragging._draggable._onUp(e.originalEvent); + // Now transfer ongoing drag action to the radius handler. + this._resizeLatLng.__vertex.dragging._draggable._onDown(e.originalEvent); + }, + + onDrawingMouseUp: function (e) { + this.commitDrawing(e); + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e); + }, + + onDrawingMouseMove: function (e) { + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e); + }, + + onDrag: function (e) { + L.Editable.PathEditor.prototype.onDrag.call(this, e); + this.feature.dragging.updateLatLng(this._resizeLatLng); + } + + }); + + // 🍂namespace Editable; 🍂class EditableMixin + // `EditableMixin` is included to `L.Polyline`, `L.Polygon`, `L.Rectangle`, `L.Circle` + // and `L.Marker`. It adds some methods to them. + // *When editing is enabled, the editor is accessible on the instance with the + // `editor` property.* + var EditableMixin = { + + createEditor: function (map) { + map = map || this._map; + var tools = (this.options.editOptions || {}).editTools || map.editTools; + if (!tools) throw Error('Unable to detect Editable instance.') + var Klass = this.options.editorClass || this.getEditorClass(tools); + return new Klass(map, this, this.options.editOptions); + }, + + // 🍂method enableEdit(map?: L.Map): this.editor + // Enable editing, by creating an editor if not existing, and then calling `enable` on it. + enableEdit: function (map) { + if (!this.editor) this.createEditor(map); + this.editor.enable(); + return this.editor; + }, + + // 🍂method editEnabled(): boolean + // Return true if current instance has an editor attached, and this editor is enabled. + editEnabled: function () { + return this.editor && this.editor.enabled(); + }, + + // 🍂method disableEdit() + // Disable editing, also remove the editor property reference. + disableEdit: function () { + if (this.editor) { + this.editor.disable(); + delete this.editor; + } + }, + + // 🍂method toggleEdit() + // Enable or disable editing, according to current status. + toggleEdit: function () { + if (this.editEnabled()) this.disableEdit(); + else this.enableEdit(); + }, + + _onEditableAdd: function () { + if (this.editor) this.enableEdit(); + } + + }; + + var PolylineMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.polylineEditorClass) ? tools.options.polylineEditorClass : L.Editable.PolylineEditor; + }, + + shapeAt: function (latlng, latlngs) { + // We can have those cases: + // - latlngs are just a flat array of latlngs, use this + // - latlngs is an array of arrays of latlngs, loop over + var shape = null; + latlngs = latlngs || this._latlngs; + if (!latlngs.length) return shape; + else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs; + else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i])) return latlngs[i]; + return shape; + }, + + isInLatLngs: function (l, latlngs) { + if (!latlngs) return false; + var i, k, len, part = [], p, + w = this._clickTolerance(); + this._projectLatlngs(latlngs, part, this._pxBounds); + part = part[0]; + p = this._map.latLngToLayerPoint(l); + + if (!this._pxBounds.contains(p)) { return false; } + for (i = 1, len = part.length, k = 0; i < len; k = i++) { + + if (L.LineUtil.pointToSegmentDistance(p, part[k], part[i]) <= w) { + return true; + } + } + return false; + } + + }; + + var PolygonMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.polygonEditorClass) ? tools.options.polygonEditorClass : L.Editable.PolygonEditor; + }, + + shapeAt: function (latlng, latlngs) { + // We can have those cases: + // - latlngs are just a flat array of latlngs, use this + // - latlngs is an array of arrays of latlngs, this is a simple polygon (maybe with holes), use the first + // - latlngs is an array of arrays of arrays, this is a multi, loop over + var shape = null; + latlngs = latlngs || this._latlngs; + if (!latlngs.length) return shape; + else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs; + else if (isFlat(latlngs[0]) && this.isInLatLngs(latlng, latlngs[0])) shape = latlngs; + else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i][0])) return latlngs[i]; + return shape; + }, + + isInLatLngs: function (l, latlngs) { + var inside = false, l1, l2, j, k, len2; + + for (j = 0, len2 = latlngs.length, k = len2 - 1; j < len2; k = j++) { + l1 = latlngs[j]; + l2 = latlngs[k]; + + if (((l1.lat > l.lat) !== (l2.lat > l.lat)) && + (l.lng < (l2.lng - l1.lng) * (l.lat - l1.lat) / (l2.lat - l1.lat) + l1.lng)) { + inside = !inside; + } + } + + return inside; + }, + + parentShape: function (shape, latlngs) { + latlngs = latlngs || this._latlngs; + if (!latlngs) return; + var idx = L.Util.indexOf(latlngs, shape); + if (idx !== -1) return latlngs; + for (var i = 0; i < latlngs.length; i++) { + idx = L.Util.indexOf(latlngs[i], shape); + if (idx !== -1) return latlngs[i]; + } + } + + }; + + + var MarkerMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.markerEditorClass) ? tools.options.markerEditorClass : L.Editable.MarkerEditor; + } + + }; + + var RectangleMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.rectangleEditorClass) ? tools.options.rectangleEditorClass : L.Editable.RectangleEditor; + } + + }; + + var CircleMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.circleEditorClass) ? tools.options.circleEditorClass : L.Editable.CircleEditor; + } + + }; + + var keepEditable = function () { + // Make sure you can remove/readd an editable layer. + this.on('add', this._onEditableAdd); + }; + + var isFlat = L.LineUtil.isFlat || L.LineUtil._flat || L.Polyline._flat; // <=> 1.1 compat. + + + if (L.Polyline) { + L.Polyline.include(EditableMixin); + L.Polyline.include(PolylineMixin); + L.Polyline.addInitHook(keepEditable); + } + if (L.Polygon) { + L.Polygon.include(EditableMixin); + L.Polygon.include(PolygonMixin); + } + if (L.Marker) { + L.Marker.include(EditableMixin); + L.Marker.include(MarkerMixin); + L.Marker.addInitHook(keepEditable); + } + if (L.Rectangle) { + L.Rectangle.include(EditableMixin); + L.Rectangle.include(RectangleMixin); + } + if (L.Circle) { + L.Circle.include(EditableMixin); + L.Circle.include(CircleMixin); + } + + L.LatLng.prototype.update = function (latlng) { + latlng = L.latLng(latlng); + this.lat = latlng.lat; + this.lng = latlng.lng; + }; + + }, window)); + + /* A Draggable that does not update the element position + and takes care of only bubbling to targetted path in Canvas mode. */ + L.PathDraggable = L.Draggable.extend({ + + initialize: function (path) { + this._path = path; + this._canvas = (path._map.getRenderer(path) instanceof L.Canvas); + var element = this._canvas ? this._path._map.getRenderer(this._path)._container : this._path._path; + L.Draggable.prototype.initialize.call(this, element, element, true); + }, + + _updatePosition: function () { + var e = {originalEvent: this._lastEvent}; + this.fire('drag', e); + }, + + _onDown: function (e) { + var first = e.touches ? e.touches[0] : e; + this._startPoint = new L.Point(first.clientX, first.clientY); + if (this._canvas && !this._path._containsPoint(this._path._map.mouseEventToLayerPoint(first))) { return; } + L.Draggable.prototype._onDown.call(this, e); + } + + }); + + + L.Handler.PathDrag = L.Handler.extend({ + + initialize: function (path) { + this._path = path; + }, + + getEvents: function () { + return { + dragstart: this._onDragStart, + drag: this._onDrag, + dragend: this._onDragEnd + }; + }, + + addHooks: function () { + if (!this._draggable) { this._draggable = new L.PathDraggable(this._path); } + this._draggable.on(this.getEvents(), this).enable(); + L.DomUtil.addClass(this._draggable._element, 'leaflet-path-draggable'); + }, + + removeHooks: function () { + this._draggable.off(this.getEvents(), this).disable(); + L.DomUtil.removeClass(this._draggable._element, 'leaflet-path-draggable'); + }, + + moved: function () { + return this._draggable && this._draggable._moved; + }, + + _onDragStart: function () { + this._startPoint = this._draggable._startPoint; + this._path + .closePopup() + .fire('movestart') + .fire('dragstart'); + }, + + _onDrag: function (e) { + var path = this._path, + event = (e.originalEvent.touches && e.originalEvent.touches.length === 1 ? e.originalEvent.touches[0] : e.originalEvent), + newPoint = L.point(event.clientX, event.clientY), + latlng = path._map.layerPointToLatLng(newPoint); + + this._offset = newPoint.subtract(this._startPoint); + this._startPoint = newPoint; + + this._path.eachLatLng(this.updateLatLng, this); + path.redraw(); + + e.latlng = latlng; + e.offset = this._offset; + path.fire('move', e) + .fire('drag', e); + }, + + _onDragEnd: function (e) { + if (this._path._bounds) this.resetBounds(); + this._path.fire('moveend') + .fire('dragend', e); + }, + + latLngToLayerPoint: function (latlng) { + // Same as map.latLngToLayerPoint, but without the round(). + var projectedPoint = this._path._map.project(L.latLng(latlng)); + return projectedPoint._subtract(this._path._map.getPixelOrigin()); + }, + + updateLatLng: function (latlng) { + var oldPoint = this.latLngToLayerPoint(latlng); + oldPoint._add(this._offset); + var newLatLng = this._path._map.layerPointToLatLng(oldPoint); + latlng.lat = newLatLng.lat; + latlng.lng = newLatLng.lng; + }, + + resetBounds: function () { + this._path._bounds = new L.LatLngBounds(); + this._path.eachLatLng(function (latlng) { + this._bounds.extend(latlng); + }); + } + + }); + + L.Path.include({ + + eachLatLng: function (callback, context) { + context = context || this; + var loop = function (latlngs) { + for (var i = 0; i < latlngs.length; i++) { + if (L.Util.isArray(latlngs[i])) loop(latlngs[i]); + else callback.call(context, latlngs[i]); + } + }; + loop(this.getLatLngs ? this.getLatLngs() : [this.getLatLng()]); + } + + }); + + L.Path.addInitHook(function () { + + this.dragging = new L.Handler.PathDrag(this); + if (this.options.draggable) { + this.once('add', function () { + this.dragging.enable(); + }); + } + + }); + + class AddAnother { + connect() { + $("[data-action='add-another']").on("click", function(event) { + event.preventDefault(); + + var templateId = $(this).data('template-id'); + + var template = document.querySelector('#' + templateId); + var clone = document.importNode(template.content, true); + + var count = $(this).closest('.form-group').find('[name="' + $(clone).find('[name]').attr('name') + '"]').length + 1; + $(clone).find('[id]').each(function(index, el) { + $(el).attr('id', $(el).attr('id') + '_' + String(count)); + }); + + $(clone).find('[for]').each(function(index, el) { + $(el).attr('for', $(el).attr('for') + '_' + String(count)); + }); + + + $(clone).insertBefore(this); + }); + } + } + + class AddNewButton { + connect() { + $("[data-expanded-add-button]").each((_i, el) => this.addExpandBehaviorToButton($(el))); + } + + addExpandBehaviorToButton(button){ + var settings = { + speed: (button.data('speed') || 450), + animate_width: (button.data('animate_width') || 425) + }; + var target = $(button.data('field-target')); + var save = $("input[data-behavior='save']", target); + var cancel = $("input[data-behavior='cancel']", target); + var input = $("input[type='text']", target); + var original_width = button.outerWidth(); + var expanded = false; + + // Animate button open when the mouse enters or + // the button is given focus (i.e. clicked/tabbed) + button.on("mouseenter focus", function(){ + expandButton(); + }); + + // Don't allow blank titles + save.on('click', function(){ + if ( inputEmpty() ) { + return false; + } + }); + + // Empty input and collapse + // button on cancel click + cancel.on('click', function(e){ + e.preventDefault(); + input.val(''); + collapseButton(); + }); + + // Collapse the button on when + // an empty input loses focus + input.on("blur", function(){ + if ( inputEmpty() ) { + collapseButton(); + } + }); + function expandButton(){ + // If this has not yet been expanded, recalculate original_width to + // handle things that may have been originally hidden. + if (!expanded) { + original_width = button.outerWidth(); + } + if(button.outerWidth() <= (original_width + 5)) { + expanded = true; + button.animate( + {width: settings.animate_width + 'px'}, settings.speed, function(){ + target.show(0, function(){ + input.focus(); + // Set the button to auto width to make + // sure it has room for any inputs + button.width("auto"); + // Explicitly set the width of the button + // so the close animation works properly + button.width(button.width()); + }); + } + ); + } + } + function collapseButton(){ + target.hide(); + button.animate({width: original_width + 'px'}, settings.speed); + } + function inputEmpty(){ + return $.trim(input.val()) == ""; + } + } + } + + class Appearance { + connect(){ + $("[data-behavior='restore-default']").each(function(){ + var hidden = $("[data-default-value]", $(this)); + var value = $($("[data-in-place-edit-target]", $(this)).data('in-place-edit-target'), $(this)); + var button = $("[data-restore-default]", $(this)); + hidden.on('blur', function(){ + if( $(this).val() == $(this).data('default-value') ) { + button.addClass('d-none'); + } else { + button.removeClass('d-none'); + } + }); + button.on('click', function(e){ + e.preventDefault(); + hidden.val(hidden.data('default-value')); + value.text(hidden.data('default-value')); + button.hide(); + }); + }); + } + } + + class BlacklightConfiguration { + connect() { + // Add Select/Deselect all button behavior + this.addCheckboxToggleBehavior(); + this.addEnableToggleBehavior(); + } + + // Add Select/Deselect all button behavior + addCheckboxToggleBehavior() { + $("[data-behavior='metadata-select']").each(function(){ + var button = $(this); + var parentCell = button.parents("th"); + var table = parentCell.closest("table"); + var columnRows = $("tr td:nth-child(" + (parentCell.index() + 1) + ")", table); + var checkboxes = $("input[type='checkbox']", columnRows); + swapSelectAllButtonText(button, columnRows); + // Add the check/uncheck behavior to the button + // and swap the button text if necessary + button.on('click', function(e){ + e.preventDefault(); + var allChecked = allCheckboxesChecked(columnRows); + columnRows.each(function(){ + $("input[type='checkbox']", $(this)).prop('checked', !allChecked); + swapSelectAllButtonText(button, columnRows); + }); + }); + // Swap button text when a checkbox value changes + checkboxes.each(function(){ + $(this).on('change', function(){ + swapSelectAllButtonText(button, columnRows); + }); + }); + }); + // Check number of checkboxes against the number of checked + // checkboxes to determine if all of them are checked or not + function allCheckboxesChecked(elements) { + return ($("input[type='checkbox']", elements).length == $("input[type='checkbox']:checked", elements).length) + } + // Swap the button text to "Deselect all" + // when all the checkboxes are checked and + // "Select all" when any are unchecked + function swapSelectAllButtonText(button, elements) { + if ( allCheckboxesChecked(elements) ) { + button.text(button.data('deselect-text')); + } else { + button.text(button.data('select-text')); + } + } + } + + addEnableToggleBehavior() { + $("[data-behavior='enable-feature']").each(function(){ + var checkbox = $(this); + var target = $($(this).data('target')); + + checkbox.on('change', function() { + if ($(this).is(':checked')) { + target.find('input:checkbox').not("[data-behavior='enable-feature']").prop('checked', true).attr('disabled', false); + } else { + target.find('input:checkbox').not("[data-behavior='enable-feature']").prop('checked', false).attr('disabled', true); + } + }); + }); + } + } + + class CopyEmailAddress { + connect() { + new Clipboard('.copy-email-addresses'); + } + } + + class Iiif { + constructor(manifestUrl, manifest) { + this.manifestUrl = manifestUrl; + this.manifest = manifest; + } + + sequences() { + var it = {}; + var context = this; + it[Symbol.iterator] = function*() { + for (let sequence of context.manifest.sequences) { + yield sequence; + } }; + return it; + } + + canvases() { + var it = {}; + var context = this; + it[Symbol.iterator] = function*() { + for (let sequence of context.sequences()) { + for (let canvas of sequence.canvases) { + yield canvas; + } + } + }; + return it; + } + + images() { + var it = {}; + var context = this; + it[Symbol.iterator] = function*() { + for (let canvas of context.canvases()) { + for (let image of canvas.images) { + var iiifService = image.resource.service['@id']; + yield { + 'thumb': iiifService + '/full/!100,100/0/default.jpg', + 'tilesource': iiifService + '/info.json', + 'manifest': context.manifestUrl, + 'canvasId': canvas['@id'], + 'imageId': image['@id'] + }; + } + } + }; + return it; + } + + imagesArray() { + return Array.from(this.images()) + } + } + + function addImageSelector(input, panel, manifestUrl, initialize) { + if (!manifestUrl) { + showNonIiifAlert(input); + return; + } + var cropper = input.data('iiifCropper'); + $.ajax(manifestUrl).done( + function(manifest) { + var iiifManifest = new Iiif(manifestUrl, manifest); + + var thumbs = iiifManifest.imagesArray(); + + hideNonIiifAlert(input); + + if (initialize) { + cropper.setIiifFields(thumbs[0]); + panel.multiImageSelector(); // Clears out existing selector + } + + if(thumbs.length > 1) { + panel.show(); + panel.multiImageSelector(thumbs, function(selectorImage) { + cropper.setIiifFields(selectorImage); + }, cropper.iiifImageField.val()); + } + } + ); + } + + function showNonIiifAlert(input){ + input.parent().prev('[data-behavior="non-iiif-alert"]').show(); + } + + function hideNonIiifAlert(input){ + input.parent().prev('[data-behavior="non-iiif-alert"]').hide(); + } + + const Spotlight$1 = function() { + var buffer = []; + return { + onLoad: function(func) { + buffer.push(func); + }, + + activate: function() { + for(var i = 0; i < buffer.length; i++) { + buffer[i].call(); + } + }, + csrfToken: function () { + return document.querySelector('meta[name=csrf-token]')?.content + }, + ZprLinks: { + close: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z\"/></svg>", + zoomIn: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><path d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zm.5-7H9v2H7v1h2v2h1v-2h2V9h-2z\"/></svg>\n", + zoomOut: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><path d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7V9z\"/></svg>\n" + } + }; + }(); + + // This allows us to configure Spotlight in app/views/layouts/base.html.erb + window.Spotlight = Spotlight$1; + + Blacklight.onLoad(function() { + Spotlight$1.activate(); + }); + + class Crop { + constructor(cropArea) { + this.cropArea = cropArea; + this.cropArea.data('iiifCropper', this); + this.cropSelector = '[data-cropper="' + cropArea.data('cropperKey') + '"]'; + this.cropTool = $(this.cropSelector); + this.formPrefix = this.cropTool.data('form-prefix'); + this.iiifUrlField = $('#' + this.formPrefix + '_iiif_tilesource'); + this.iiifRegionField = $('#' + this.formPrefix + '_iiif_region'); + this.iiifManifestField = $('#' + this.formPrefix + '_iiif_manifest_url'); + this.iiifCanvasField = $('#' + this.formPrefix + '_iiif_canvas_id'); + this.iiifImageField = $('#' + this.formPrefix + '_iiif_image_id'); + + this.form = cropArea.closest('form'); + this.tileSource = null; + } + + // Render the cropper environment and add hooks into the autocomplete and upload forms + render() { + this.setupAutoCompletes(); + this.setupAjaxFileUpload(); + this.setupExistingIiifCropper(); + } + + // Setup the cropper on page load if the field + // that holds the IIIF url is populated + setupExistingIiifCropper() { + if(this.iiifUrlField.val() === '') { + return; + } + + this.addImageSelectorToExistingCropTool(); + this.setTileSource(this.iiifUrlField.val()); + } + + // Display the IIIF Cropper map with the current IIIF Layer (and cropbox, once the layer is available) + setupIiifCropper() { + this.loaded = false; + + this.renderCropperMap(); + + if (this.imageLayer) { + // Force a broken layer's container to be an element before removing. + // Code in leaflet-iiif land calls delete on the image layer's container when removing, + // which errors if there is an issue fetching the info.json and stops further necessary steps to execute. + if(!this.imageLayer._container) { + this.imageLayer._container = $('<div></div>'); + } + this.cropperMap.removeLayer(this.imageLayer); + } + + this.imageLayer = L.tileLayer.iiif(this.tileSource).addTo(this.cropperMap); + + var self = this; + this.imageLayer.on('load', function() { + if (!self.loaded) { + var region = self.getCropRegion(); + self.positionIiifCropBox(region); + self.loaded = true; + } + }); + + this.cropArea.data('initiallyVisible', this.cropArea.is(':visible')); + } + + // Get (or initialize) the current crop region from the form data + getCropRegion() { + var regionFieldValue = this.iiifRegionField.val(); + if(!regionFieldValue || regionFieldValue === '') { + var region = this.defaultCropRegion(); + this.iiifRegionField.val(region); + return region; + } else { + return regionFieldValue.split(','); + } + } + + // Calculate a default crop region in the center of the image using the correct aspect ratio + defaultCropRegion() { + var imageWidth = this.imageLayer.x; + var imageHeight = this.imageLayer.y; + + var boxWidth = Math.floor(imageWidth / 2); + var boxHeight = Math.floor(boxWidth / this.aspectRatio()); + + return [ + Math.floor((imageWidth - boxWidth) / 2), + Math.floor((imageHeight - boxHeight) / 2), + boxWidth, + boxHeight + ]; + } + + // Calculate the required aspect ratio for the crop area + aspectRatio() { + var cropWidth = parseInt(this.cropArea.data('crop-width')); + var cropHeight = parseInt(this.cropArea.data('crop-height')); + return cropWidth / cropHeight; + } + + // Position the IIIF Crop Box at the given IIIF region + positionIiifCropBox(region) { + var bounds = this.unprojectIIIFRegionToBounds(region); + + if (!this.cropBox) { + this.renderCropBox(bounds); + } + + this.cropBox.setBounds(bounds); + this.cropperMap.invalidateSize(); + this.cropperMap.fitBounds(bounds); + + this.cropBox.editor.editLayer.clearLayers(); + this.cropBox.editor.refresh(); + this.cropBox.editor.initVertexMarkers(); + } + + // Set all of the various input fields to + // the appropriate IIIF URL or identifier + setIiifFields(iiifObject) { + this.setTileSource(iiifObject.tilesource); + this.iiifManifestField.val(iiifObject.manifest); + this.iiifCanvasField.val(iiifObject.canvasId); + this.iiifImageField.val(iiifObject.imageId); + } + + // Set the Crop tileSource and setup the cropper + setTileSource(source) { + if (source == this.tileSource) { + return; + } + + if (source === null || source === undefined) { + console.error('No tilesource provided when setting up IIIF Cropper'); + return; + } + + if (this.cropBox) { + this.iiifRegionField.val(""); + } + + this.tileSource = source; + this.iiifUrlField.val(source); + this.setupIiifCropper(); + } + + // Render the Leaflet Map into the crop area + renderCropperMap() { + if (this.cropperMap) { + return; + } + this.cropperMap = L.map(this.cropArea.attr('id'), { + editable: true, + center: [0, 0], + crs: L.CRS.Simple, + zoom: 0, + editOptions: { + rectangleEditorClass: this.aspectRatioPreservingRectangleEditor(this.aspectRatio()) + } + }); + this.invalidateMapSizeOnTabToggle(); + } + + // Render the crop box (a Leaflet editable rectangle) onto the canvas + renderCropBox(initialBounds) { + this.cropBox = L.rectangle(initialBounds); + this.cropBox.addTo(this.cropperMap); + this.cropBox.enableEdit(); + this.cropBox.on('dblclick', L.DomEvent.stop).on('dblclick', this.cropBox.toggleEdit); + + var self = this; + this.cropperMap.on('editable:dragend editable:vertex:dragend', function(e) { + var bounds = e.layer.getBounds(); + var region = self.projectBoundsToIIIFRegion(bounds); + + self.iiifRegionField.val(region.join(',')); + }); + } + + // Get the maximum zoom level for the IIIF Layer (always 1:1 image pixel to canvas?) + maxZoom() { + if(this.imageLayer) { + return this.imageLayer.maxZoom; + } + } + + // Take a Leaflet LatLngBounds object and transform it into a IIIF [x, y, w, h] region + projectBoundsToIIIFRegion(bounds) { + var min = this.cropperMap.project(bounds.getNorthWest(), this.maxZoom()); + var max = this.cropperMap.project(bounds.getSouthEast(), this.maxZoom()); + return [ + Math.max(Math.floor(min.x), 0), + Math.max(Math.floor(min.y), 0), + Math.floor(max.x - min.x), + Math.floor(max.y - min.y) + ]; + } + + // Take a IIIF [x, y, w, h] region and transform it into a Leaflet LatLngBounds + unprojectIIIFRegionToBounds(region) { + var minPoint = L.point(parseInt(region[0]), parseInt(region[1])); + var maxPoint = L.point(parseInt(region[0]) + parseInt(region[2]), parseInt(region[1]) + parseInt(region[3])); + + var min = this.cropperMap.unproject(minPoint, this.maxZoom()); + var max = this.cropperMap.unproject(maxPoint, this.maxZoom()); + return L.latLngBounds(min, max); + } + + // TODO: Add accessors to update hidden inputs with IIIF uri/ids? + + // Setup autocomplete inputs to have the iiif_cropper context + setupAutoCompletes() { + var input = $('[data-behavior="autocomplete"]', this.cropTool); + input.data('iiifCropper', this); + } + + setupAjaxFileUpload() { + this.fileInput = $('input[type="file"]', this.cropTool); + this.fileInput.change(() => this.uploadFile()); + } + + addImageSelectorToExistingCropTool() { + if(this.iiifManifestField.val() === '') { + return; + } + + var input = $('[data-behavior="autocomplete"]', this.cropTool); + var panel = $(input.data('target-panel')); + + addImageSelector(input, panel, this.iiifManifestField.val(), !this.iiifImageField.val()); + } + + invalidateMapSizeOnTabToggle() { + var tabs = $('[role="tablist"]', this.form); + var self = this; + tabs.on('shown.bs.tab', function() { + if(self.cropArea.data('initiallyVisible') === false && self.cropArea.is(':visible')) { + self.cropperMap.invalidateSize(); + // Because the map size is 0,0 when image is loading (not visible) we need to refit the bounds of the layer + self.imageLayer._fitBounds(); + self.cropArea.data('initiallyVisible', null); + } + }); + } + + // Get all the form data with the exception of the _method field. + getData() { + var data = new FormData(this.form[0]); + data.append('_method', null); + return data; + } + + uploadFile() { + var url = this.fileInput.data('endpoint'); + // Every post creates a new image/masthead. + // Because they create IIIF urls which are heavily cached. + $.ajax({ + url: url, //Server script to process data + type: 'POST', + success: (data, stat, xhr) => this.successHandler(data, stat, xhr), + // error: errorHandler, + // Form data + data: this.getData(), + headers: { + 'X-CSRF-Token': Spotlight$1.csrfToken() || '' + }, + //Options to tell jQuery not to process data or worry about content-type. + cache: false, + contentType: false, + processData: false + }); + } + + successHandler(data, stat, xhr) { + this.setIiifFields({ tilesource: data.tilesource }); + this.setUploadId(data.id); + } + + setUploadId(id) { + $('#' + this.formPrefix + "_upload_id").val(id); + } + + aspectRatioPreservingRectangleEditor(aspect) { + return L.Editable.RectangleEditor.extend({ + extendBounds: function (e) { + var index = e.vertex.getIndex(), + next = e.vertex.getNext(), + previous = e.vertex.getPrevious(), + oppositeIndex = (index + 2) % 4, + opposite = e.vertex.latlngs[oppositeIndex]; + + if ((index % 2) == 1) { + // calculate horiz. displacement + e.latlng.update([opposite.lat + ((1 / aspect) * (opposite.lng - e.latlng.lng)), e.latlng.lng]); + } else { + // calculate vert. displacement + e.latlng.update([e.latlng.lat, (opposite.lng - (aspect * (opposite.lat - e.latlng.lat)))]); + } + var bounds = new L.LatLngBounds(e.latlng, opposite); + // Update latlngs by hand to preserve order. + previous.latlng.update([e.latlng.lat, opposite.lng]); + next.latlng.update([opposite.lat, e.latlng.lng]); + this.updateBounds(bounds); + this.refreshVertexMarkers(); + } + }); + } + } + + class Croppable { + connect() { + $('[data-behavior="iiif-cropper"]').each(function() { + var cropElement = $(this); + new Crop(cropElement).render(); + }); + } + } + + /* + Simple plugin add edit-in-place behavior + */ + class EditInPlace { + connect() { + $('[data-in-place-edit-target]').each(function() { + $(this).on('click.inplaceedit', function() { + var $label = $(this).find($(this).data('in-place-edit-target')); + var $input = $(this).find($(this).data('in-place-edit-field-target')); + + // hide the edit-in-place affordance icon while in edit mode + $(this).addClass('hide-edit-icon'); + $label.hide(); + $input.val($label.text()); + $input.attr('type', 'text'); + $input.select(); + $input.focus(); + + $input.on('keypress', function(e) { + if(e.which == 13) { + $input.trigger('blur.inplaceedit'); + return false; + } + }); + + $input.on('blur.inplaceedit', function() { + var value = $input.val(); + + if ($.trim(value).length == 0) { + $input.val($label.text()); + } else { + $label.text(value); + } + + $label.show(); + $input.attr('type', 'hidden'); + // when leaving edit mode, should no longer hide edit-in-place affordance icon + $("[data-in-place-edit-target]").removeClass('hide-edit-icon'); + + return false; + }); + + return false; + }); + }); + } + } + + class ExhibitTagAutocomplete { + connect() { + $('[data-autocomplete-tag="true"]').each(function(_i, el) { + var $el = $(el); + // By default tags input binds on page ready to [data-role=tagsinput], + // however, that doesn't work with Turbolinks. So we init manually: + $el.tagsinput(); + + var tags = new Bloodhound({ + datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.name); }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + limit: 100, + prefetch: { + url: $el.data('autocomplete-url'), + ttl: 1, + filter: function(list) { + // Let the dom know that the response has been returned + $el.attr('data-autocomplete-fetched', true); + return $.map(list, function(tag) { return { name: tag }; }); + } + } + }); + + tags.initialize(); + + $el.tagsinput('input').typeahead({highlight: true, hint: false}, { + name: 'tags', + displayKey: 'name', + source: tags.ttAdapter() + }).bind('typeahead:selected', $.proxy(function (obj, datum) { + $el.tagsinput('add', datum.name); + $el.tagsinput('input').typeahead('val', ''); + })).bind('blur', function() { + $el.tagsinput('add', $el.tagsinput('input').typeahead('val')); + $el.tagsinput('input').typeahead('val', ''); + }); + }); + } + } + + class Exhibits { + connect() { + // auto-fill the exhibit slug on the new exhibit form + $('#new_exhibit').each(function() { + $('#exhibit_title').on('change keyup', function() { + $('#exhibit_slug').attr('placeholder', URLify($(this).val(), $(this).val().length)); + }); + + $('#exhibit_slug').on('focus', function() { + if ($(this).val() === '') { + $(this).val($(this).attr('placeholder')); + } + }); + }); + + $("#another-email").on("click", function(e) { + e.preventDefault(); + + var container = $(this).closest('.form-group'); + var contacts = container.find('.contact'); + var inputContainer = contacts.first().clone(); + + // wipe out any values from the inputs + inputContainer.find('input').each(function() { + $(this).val(''); + $(this).attr('id', $(this).attr('id').replace('0', contacts.length)); + $(this).attr('name', $(this).attr('name').replace('0', contacts.length)); + if ($(this).attr('aria-label')) { + $(this).attr('aria-label', $(this).attr('aria-label').replace('1', contacts.length + 1)); + } + }); + + inputContainer.find('.contact-email-delete-wrapper').remove(); + inputContainer.find('.confirmation-status').remove(); + + // bootstrap does not render input-groups with only one value in them correctly. + inputContainer.find('.input-group input:only-child').closest('.input-group').removeClass('input-group'); + + $(inputContainer).insertAfter(contacts.last()); + }); + + $('.contact-email-delete').on('ajax:success', function() { + $(this).closest('.contact').fadeOut(250, function() { $(this).remove(); }); + }); + + $('.contact-email-delete').on('ajax:error', function(event, _xhr, _status, error) { + var errSpan = $(this).closest('.contact').find('.contact-email-delete-error'); + errSpan.show(); + errSpan.find('.error-msg').first().text(error || event.detail[1]); + }); + + $('.btn-with-tooltip').tooltip(); + + // Put focus in saved search title input when Save this search modal is shown + $('#save-modal').on('shown.bs.modal', function () { + $('#search_title').focus(); + }); + } + } + + (function($, undefined$1) { + + /* + * SerializedForm is built as a singleton jQuery plugin. It needs to be able to + * handle instantiation from multiple sources, and use the [data-form-observer] + * as global state object. + */ + $.SerializedForm = function () { + var $serializedForm; + var plugin = this; + + // Store form serialization in data attribute + function serializeFormStatus () { + $serializedForm.data('serialized-form', formSerialization($serializedForm)); + } + + // Do custom serialization of the sir-trevor form data. This needs to be a + // passed in argument for comparison later on. + function formSerialization (form) { + var content_editable = []; + var i = 0; + $("[contenteditable='true']", form).each(function(){ + content_editable.push('&contenteditable_' + i + '=' + $(this).text()); + }); + return form.serialize() + content_editable.join(); + } + + // Unbind observing form on submit (which we have to do because of turbolinks) + function unbindObservedFormSubmit () { + $serializedForm.on('submit', function () { + $(this).data('being-submitted', true); + }); + } + + // Get the stored serialized form status + function serializedFormStatus () { + return $serializedForm.data('serialized-form'); + } + + // Check all observed forms on page for status change + plugin.observedFormsStatusHasChanged = function () { + var unsaved_changes = false; + $('[data-form-observer]').each(function (){ + if ( !$(this).data("being-submitted") ) { + if (serializedFormStatus() != formSerialization($(this))) { + unsaved_changes = true; + } + } + }); + return unsaved_changes; + }; + + function init () { + $serializedForm = $('[data-form-observer]'); + serializeFormStatus(); + unbindObservedFormSubmit(); + } + + init(); + + return plugin; + }; + })(jQuery); + + class FormObserver { + connect() { + // Instantiate the singleton SerializedForm plugin + var serializedForm = $.SerializedForm(); + $(window).on('beforeunload page:before-change turbolinks:before-visit', function(event) { + // Don't handle the same event twice #turbolinks + if (event.handled !== true) { + if ( serializedForm.observedFormsStatusHasChanged() ) { + event.handled = true; + var message = "You have unsaved changes. Are you sure you want to leave this page?"; + // There are variations in how Webkit browsers may handle this: + // https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload + if ( event.type == "beforeunload" ) { + return message; + } else { + return confirm(message) + } + } + } + }); + } + } + + class Locks { + delete_lock(el) { + $.ajax({ url: $(el).data('lock'), type: 'POST', data: { _method: "delete" }, async: false}); + $(el).removeAttr('data-lock'); + } + + connect() { + $('[data-lock]').on('click', (e) => { + this.delete_lock(e.target); + }); + } + } + + // Module to add multi-image selector to widget panels + + (function(){ + $.fn.multiImageSelector = function(image_versions, clickCallback, activeImageId) { + var changeLink = $("<a href='javascript:;'>Change</a>"), + thumbsListContainer = $("<div class='thumbs-list' style='display:none'></div>"), + thumbList = $("<ul></ul>"), + panel; + + var imageIds = $.map(image_versions, function(e) { return e['imageId']; }); + + return init(this); + + function init(el) { + panel = el; + + destroyExistingImageSelector(); + if(image_versions && image_versions.length > 1) { + addChangeLink(); + addThumbsList(); + } + } + function addChangeLink() { + $('[data-panel-image-pagination]', panel) + .html("Image <span data-current-image='true'>" + indexOf(activeImageId) + "</span> of " + image_versions.length) + .show() + .append(" ") + .append(changeLink); + addChangeLinkBehavior(); + } + + function destroyExistingImageSelector() { + var pagination = $('[data-panel-image-pagination]', panel); + pagination.html(''); + pagination.next('.' + thumbsListContainer.attr('class')).remove(); + } + + function indexOf(thumb){ + const index = imageIds.indexOf(thumb); + if (index > -1) { + return index + 1; + } else { + return 1; + } + } + function addChangeLinkBehavior() { + changeLink.on('click', function(){ + thumbsListContainer.slideToggle(); + updateThumbListWidth(); + addScrollBehavior(); + scrollToActiveThumb(); + loadVisibleThumbs(); + swapChangeLinkText($(this)); + }); + } + function updateThumbListWidth() { + var width = 0; + $('li', thumbList).each(function(){ + width += $(this).outerWidth(); + }); + thumbList.width(width + 5); + } + function loadVisibleThumbs(){ + var viewportWidth = thumbsListContainer.width(); + var width = 0; + $('li', thumbList).each(function(){ + var thisThumb = $(this), + image = $('img', thisThumb), + totalWidth = width += thisThumb.width(); + position = (thumbList.position().left + totalWidth) - thisThumb.width(); + + if(position >= 0 && position < viewportWidth) { + image.prop('src', image.data('src')); + } + }); + } + function addScrollBehavior(){ + thumbsListContainer.scrollStop(function(){ + loadVisibleThumbs(); + }); + } + function scrollToActiveThumb(){ + var halfContainerWidth = (thumbsListContainer.width() / 2), + activeThumbLeftPosition = ($('.active', thumbList).position() || $('li', thumbList).first().position()).left, + halfActiveThumbWidth = ($('.active', thumbList).width() / 2); + thumbsListContainer.scrollLeft( + (activeThumbLeftPosition - halfContainerWidth) + halfActiveThumbWidth + ); + } + function addThumbsList() { + addThumbsToList(); + updateActiveThumb(); + $('.card-header', panel).append( + thumbsListContainer.append( + thumbList + ) + ); + } + function updateActiveThumb(){ + $('li', thumbList).each(function(){ + var item = $(this); + if($('img', item).data('image-id') == activeImageId){ + item.addClass('active'); + } + }); + } + function swapChangeLinkText(link){ + link.text( + link.text() == 'Change' ? 'Close' : 'Change' + ); + } + + function addThumbsToList(){ + $.each(image_versions, function(i){ + var listItem = $('<li data-index="' + i + '"><a href="javascript:;"><img src="' + image_versions[i]['thumb'] +'" data-image-id="' + image_versions[i]['imageId'] +'" /></a></li>'); + listItem.on('click', function(){ + // get the current image id + $('img', $(this)).data('image-id'); + var src = $('img', $(this)).attr('src'); + + if (typeof clickCallback === 'function' ) { + clickCallback(image_versions[i]); + } + + // mark the current selection as active + $('li.active', thumbList).removeClass('active'); + $(this).addClass('active'); + + // update the multi-image selector image + $(".pic img.img-thumbnail", panel).attr("src", src); + + $('[data-panel-image-pagination] [data-current-image]', panel).text( + $('li', thumbList).index($(this)) + 1 + ); + scrollToActiveThumb(); + }); + $("img", listItem).on('load', function() { + updateThumbListWidth(); + }); + thumbList.append(listItem); + }); + } + }; + + })(jQuery); + + // source: http://stackoverflow.com/questions/14035083/jquery-bind-event-on-scroll-stops + jQuery.fn.scrollStop = function(callback) { + $(this).scroll(function() { + var self = this, + $this = $(self); + + if ($this.data('scrollTimeout')) { + clearTimeout($this.data('scrollTimeout')); + } + + $this.data('scrollTimeout', setTimeout(callback, 250, self)); + }); + }; + + // Place all the behaviors and hooks related to the matching controller here. + + class Pages { + connect(){ + SirTrevor.setDefaults({ + iconUrl: Spotlight$1.sirTrevorIcon, + uploadUrl: $('[data-attachment-endpoint]').data('attachment-endpoint'), + ajaxOptions: { + headers: { + 'X-CSRF-Token': Spotlight$1.csrfToken() || '' + }, + credentials: 'same-origin' + } + }); + + SirTrevor.Blocks.Heading.prototype.toolbarEnabled = true; + SirTrevor.Blocks.Quote.prototype.toolbarEnabled = true; + SirTrevor.Blocks.Text.prototype.toolbarEnabled = true; + + var instance = $('.js-st-instance').first(); + + if (instance.length) { + var editor = new SirTrevor.Editor({ + el: instance[0], + blockTypes: instance.data('blockTypes'), + defaultType:["Text"], + onEditorRender: function() { + $.SerializedForm(); + }, + blockTypeLimits: { + "SearchResults": 1 + } + }); + + editor.blockControls = Spotlight$1.BlockControls.create(editor); + + new Spotlight$1.BlockLimits(editor).enforceLimits(editor); + } + } + } + + class ProgressMonitor { + connect() { + var monitorElements = $('[data-behavior="progress-panel"]'); + var defaultRefreshRate = 3000; + var panelContainer; + var pollers = []; + + $(monitorElements).each(function() { + panelContainer = $(this); + panelContainer.hide(); + var monitorUrl = panelContainer.data('monitorUrl'); + var refreshRate = panelContainer.data('refreshRate') || defaultRefreshRate; + pollers.push( + setInterval(function() { + checkMonitorUrl(monitorUrl); + }, refreshRate) + ); + }); + + // Clear the intervals on turbolink:click event (e.g. when the user navigates away from the page) + $(document).on('turbolinks:click', function() { + if (pollers.length > 0) { + $.each(pollers, function() { + clearInterval(this); + }); + pollers = []; + } + }); + + function checkMonitorUrl(url) { + $.ajax(url).done(success).fail(fail); + } + + function success(data) { + if (data.recently_in_progress) { + updateMonitorPanel(data); + monitorPanel().show(); + } else { + monitorPanel().hide(); + } + } + + function fail() { monitorPanel().hide(); } + + function updateMonitorPanel(data) { + panelStartDate().text(data.started_at); + panelCurrentDate().text(data.updated_at); + panelCompletedDate().text(data.updated_at); + panelCurrent().text(data.completed); + setPanelCompleted(data.finished); + updatePanelTotals(data); + updatePanelErrorMessage(data); + updateProgressBar(data); + + panelContainer.show(); + } + + function updateProgressBar(data) { + var percentage = calculatePercentage(data); + progressBar() + .attr('aria-valuemax', data.total) + .attr('aria-valuenow', percentage) + .css('width', percentage + '%') + .text(percentage + '%'); + + if (data.finished) { + progressBar().removeClass('active').removeClass('progress-bar-striped'); + } + } + + function updatePanelErrorMessage(data) { + // We currently do not store this state, + // but with this code we can in the future. + if ( data.errored ) { + panelErrorMessage().show(); + } else { + panelErrorMessage().hide(); + } + } + + function updatePanelTotals(data) { + panelTotals().each(function() { + $(this).text(data.total); + }); + } + + function calculatePercentage(data) { + if (data.total == 0) return 0; + return Math.floor((data.completed / data.total) * 100); + } + + function monitorPanel() { + return panelContainer.find('.index-status'); + } + + function panelStartDate() { + return monitorPanel() + .find('[data-behavior="monitor-start"]') + .find('[data-behavior="date"]'); + } + + function panelCurrentDate() { + return monitorPanel() + .find('[data-behavior="monitor-current"]') + .find('[data-behavior="date"]'); + } + + function panelCompletedDate() { + return monitorPanel() + .find('[data-behavior="monitor-completed"]') + .find('[data-behavior="date"]'); + } + + function panelTotals() { + return monitorPanel().find('[data-behavior="total"]'); + } + + function panelCurrent() { + return monitorPanel() + .find('[data-behavior="monitor-current"]') + .find('[data-behavior="completed"]'); + } + + function progressBar() { + return monitorPanel().find('.progress-bar'); + } + + function panelErrorMessage() { + return monitorPanel().find('[data-behavior="monitor-error"]'); + } + + function setPanelCompleted(finished) { + var panel = monitorPanel().find('[data-behavior="monitor-completed"]'); + + if (finished) { + panel.show(); + } else { + panel.hide(); + } + } + + return this; + } + } + + class ReadonlyCheckbox { + connect() { + // Don't allow unchecking of checkboxes with the data-readonly attribute + $("input[type='checkbox'][data-readonly]").on("click", function(event) { + event.preventDefault(); + }); + } + } + + (function($){ + $.fn.spotlightSearchTypeAhead = function( options ) { + $.each(this, function(){ + addAutocompleteBehavior($(this)); + }); + + function addAutocompleteBehavior( typeAheadInput, settings ) { + var settings = $.extend({ + displayKey: 'title', + minLength: 0, + highlight: (typeAheadInput.data('autocomplete-highlight') || true), + hint: (typeAheadInput.data('autocomplete-hint') || false), + autoselect: (typeAheadInput.data('autocomplete-autoselect') || true) + }, options); + + typeAheadInput.typeahead(settings, { + displayKey: settings.displayKey, + source: settings.bloodhound.ttAdapter(), + templates: { + suggestion: Handlebars.compile(settings.template) + } + }); + } + return this; + }; + })( jQuery ); + + function itemsBloodhound() { + var results = new Bloodhound({ + datumTokenizer: function(d) { + return Bloodhound.tokenizers.whitespace(d.title); + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + limit: 100, + remote: { + url: $('form[data-autocomplete-exhibit-catalog-path]').data('autocomplete-exhibit-catalog-path').replace("%25QUERY", "%QUERY"), + filter: function(response) { + return $.map(response['docs'], function(doc) { + return doc; + }) + } + } + }); + results.initialize(); + return results; + } + function itemsTemplate() { + return '<div class="autocomplete-item{{#if private}} blacklight-private{{/if}}">{{#if thumbnail}}<div class="document-thumbnail"><img class="img-thumbnail" src="{{thumbnail}}" /></div>{{/if}}<span class="autocomplete-title">{{title}}</span><br/><small>&nbsp;&nbsp;{{description}}</small></div>'; + } + + function addAutocompletetoFeaturedImage(){ + if($('[data-featured-image-typeahead]').length > 0) { + $('[data-featured-image-typeahead]').spotlightSearchTypeAhead({bloodhound: itemsBloodhound(), template: itemsTemplate()}).on('click', function() { + $(this).select(); + }).on('typeahead:selected typeahead:autocompleted', function(e, data) { + var panel = $($(this).data('target-panel')); + addImageSelector($(this), panel, data.iiif_manifest, true); + $($(this).data('id-field')).val(data['global_id']); + $(this).attr('type', 'text'); + }); + } + } + + /* + Simple plugin to select form elements + when other elements are clicked. + */ + (function($) { + $.fn.selectRelatedInput = function() { + var clickElements = this; + + $(clickElements).each(function() { + var target = $($(this).data('input-select-target')); + + var event; + + if ($(this).is("select")) { + event = 'change'; + } else { + event = 'click'; + } + + $(this).on(event, function() { + if (target.is(":checkbox") || target.is(":radio")) { + target.prop('checked', true); + } else { + target.focus(); + } + }); + }); + + return this; + }; + })(jQuery); + + class SelectRelatedInput { + connect() { + $('[data-input-select-target]').selectRelatedInput(); + } + } + + const Module = (function() { + var nestableSelector = '[data-behavior="nestable"]'; + return { + init: function(selector){ + + $(selector || nestableSelector).each(function(){ + // Because the Rails helper will not maintain the case that Nestable + // expects, we just need to do this manual conversion. :( + var data = $(this).data(); + data.expandBtnHTML = data.expandBtnHtml; + data.collapseBtnHTML = data.collapseBtnHtml; + $(this).nestable(data); + updateWeightsAndRelationships($(this)); + }); + } + }; + function updateWeightsAndRelationships(nestedList){ + nestedList.on('change', function(event){ + var container = $(event.currentTarget); + var data = $(this).nestable('serialize'); + var weight = 0; + for(var i in data){ + var parent_id = data[i]['id']; + const parent_node = findNode(parent_id, container); + setWeight(parent_node, weight++); + if(data[i]['children']){ + var children = data[i]['children']; + for(var child in children){ + var id = children[child]['id']; + child_node = findNode(id, container); + setWeight(child_node, weight++); + setParent(child_node, parent_id); + } + } else { + setParent(parent_node, ""); + } + } + }); + + } + function findNode(id, container) { + return container.find("[data-id="+id+"]"); + } + + function setWeight(node, weight) { + weight_field(node).val(weight); + } + + function setParent(node, parent_id) { + parent_page_field(node).val(parent_id); + } + + /* find the input element with data-property="weight" that is nested under the given node */ + function weight_field(node) { + return find_property(node, "weight"); + } + + /* find the input element with data-property="parent_page" that is nested under the given node */ + function parent_page_field(node){ + return find_property(node, "parent_page"); + } + + function find_property(node, property) { + return node.find("input[data-property=" + property + "]"); + } + })(); + + class Tabs { + connect() { + if ($('[role=tabpanel]').length > 0 && window.location.hash) { + var tabpanel = $(window.location.hash).closest('[role=tabpanel]'); + $('a[role=tab][href="#'+tabpanel.attr('id')+'"]').tab('show'); + } + } + } + + // translationProgress is a plugin that updates the "3/14" progress + // counters in the tabs of the translation adminstration dashboard. + // This works by counting the number of progress items and translations + // present (indicated by data attributes) in each tab's content + class TranslationProgress { + connect() { + $('[data-behavior="translation-progress"]').each(function(){ + var currentTab = $(this); + var tabName = $(this).attr('aria-controls'); + var translationFields = $('#' + tabName).find('[data-translation-progress-item="true"]'); + var completedTranslations = $('#' + tabName).find('[data-translation-present="true"]'); + + currentTab.find('span').text(completedTranslations.length + '/' + translationFields.length); + }); + } + } + + /* + NOTE: this is copied & adapted from BL8's checkbox_submit.js in order to have + it accessible in a BL7-based spotlight. Once we drop support for BL7, this file + can be deleted and we can change visibility_toggle.es6 to import CheckboxSubmit + from Blacklight. + + See https://github.com/projectblacklight/blacklight/blob/main/app/javascript/blacklight/checkbox_submit.js + */ + class CheckboxSubmit { + constructor(form) { + this.form = form; + } + + async clicked(evt) { + this.spanTarget.innerHTML = this.form.getAttribute('data-inprogress'); + this.labelTarget.setAttribute('disabled', 'disabled'); + this.checkboxTarget.setAttribute('disabled', 'disabled'); + const csrfMeta = document.querySelector('meta[name=csrf-token]'); + const response = await fetch(this.formTarget.getAttribute('action'), { + body: new FormData(this.formTarget), + method: this.formTarget.getAttribute('method').toUpperCase(), + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': csrfMeta ? csrfMeta.content : '' + } + }); + this.labelTarget.removeAttribute('disabled'); + this.checkboxTarget.removeAttribute('disabled'); + if (response.ok) { + this.updateStateFor(!this.checked); + // Not used for our case in Spotlight (visibility toggle) + // const json = await response.json() + // document.querySelector('[data-role=bookmark-counter]').innerHTML = json.bookmarks.count + } else { + alert('Error'); + } + } + + get checked() { + return (this.form.querySelectorAll('input[name=_method][value=delete]').length != 0) + } + + get formTarget() { + return this.form + } + + get labelTarget() { + return this.form.querySelector('[data-checkboxsubmit-target="label"]') + } + + get checkboxTarget() { + return this.form.querySelector('[data-checkboxsubmit-target="checkbox"]') + } + + get spanTarget() { + return this.form.querySelector('[data-checkboxsubmit-target="span"]') + } + + updateStateFor(state) { + this.checkboxTarget.checked = state; + + if (state) { + this.labelTarget.classList.add('checked'); + //Set the Rails hidden field that fakes an HTTP verb + //properly for current state action. + this.formTarget.querySelector('input[name=_method]').value = 'delete'; + this.spanTarget.innerHTML = this.form.getAttribute('data-present'); + } else { + this.labelTarget.classList.remove('checked'); + this.formTarget.querySelector('input[name=_method]').value = 'put'; + this.spanTarget.innerHTML = this.form.getAttribute('data-absent'); + } + } + } + + // Visibility toggle for items in an exhibit, based on Blacklight's bookmark toggle + + const VisibilityToggle = (e) => { + if (e.target.matches('[data-checkboxsubmit-target="checkbox"]')) { + const form = e.target.closest('form'); + if (form) { + new CheckboxSubmit(form).clicked(e); + + // Add/remove the "private" label to the document row when visibility is toggled + const docRow = form.closest('tr'); + if (docRow) docRow.classList.toggle('blacklight-private'); + } + } + }; + + VisibilityToggle.selector = 'form.visibility-toggle'; + + document.addEventListener('click', VisibilityToggle); + + class Users { + connect() { + var container; + function edit_user(event) { + event.preventDefault(); + $(this).closest('tr').hide(); + id = $(this).attr('data-target'); + edit_view = $("[data-edit-for='"+id+"']", container).show(); + $.each(edit_view.find('input[type="text"], select'), function() { + // Cache original values incase editing is canceled + $(this).data('orig', $(this).val()); + }); + } + + function cancel_edit(event) { + event.preventDefault(); + id = $(this).closest('tr').attr('data-edit-for'); + edit_view = $("[data-edit-for='"+id+"']", container).hide(); + clear_errors(edit_view); + rollback_changes(edit_view); + $("[data-show-for='"+id+"']", container).show(); + } + + function clear_errors(element) { + element.find('.has-error') + .removeClass('has-error') + .find('.form-text') + .remove(); // Remove the error messages + } + + function rollback_changes(element) { + $.each(element.find('input[type="text"], select'), function() { + $(this).val($(this).data('orig')).trigger('change'); + }); + } + + function destroy_user(event) { + id = $(this).attr('data-target'); + $("[data-destroy-for='"+id+"']", container).val('1'); + } + + function new_user(event) { + event.preventDefault(); + edit_view = $("[data-edit-for='new']", container).show(); + $.each(edit_view.find('input[type="text"], select'), function() { + // Cache original values incase editing is canceled + $(this).data('orig', $(this).val()); + }); + } + + function open_errors() { + const edit_row = container.find('.has-error').closest('[data-edit-for]'); + edit_row.show(); + // The following row has the controls, so show it too. + edit_row.next().show(); + } + + $('.edit_exhibit, .admin-users').each(function() { + + container = $(this); + $('[data-edit-for]', container).hide(); + open_errors(); + $("[data-behavior='edit-user']", container).on('click', edit_user); + $("[data-behavior='cancel-edit']", container).on('click', cancel_edit); + $("[data-behavior='destroy-user']", container).on('click', destroy_user); + $("[data-behavior='new-user']", container).on('click', new_user); + }); + } + } + + (function ($){ + SirTrevor.BlockMixins.Autocompleteable = { + mixinName: "Autocompleteable", + preload: true, + + initializeAutocompleteable: function() { + this.on("onRender", this.addAutocompletetoSirTrevorForm); + + if (this['autocomplete_url'] === undefined) { + this.autocomplete_url = function() { return $('form[data-autocomplete-url]').data('autocomplete-url').replace("%25QUERY", "%QUERY"); }; + } + + if (this['autocomplete_template'] === undefined) { + this.autocomplete_url = function() { return '<div class="autocomplete-item{{#if private}} blacklight-private{{/if}}">{{#if thumbnail}}<div class="document-thumbnail"><img class="img-thumbnail" src="{{thumbnail}}" /></div>{{/if}}<span class="autocomplete-title">{{title}}</span><br/><small>&nbsp;&nbsp;{{description}}</small></div>' }; + } + + if (this['transform_autocomplete_results'] === undefined) { + this.transform_autocomplete_results = (val) => val; + } + + if (this['autocomplete_control'] === undefined) { + this.autocomplete_control = function() { return `<input type="text" class="st-input-string form-control item-input-field" data-twitter-typeahead="true" placeholder="${i18n.t("blocks:autocompleteable:placeholder")}"/>` }; + } + + if (this['bloodhoundOptions'] === undefined) { + this.bloodhoundOptions = function() { + return { + remote: { + url: this.autocomplete_url(), + filter: this.transform_autocomplete_results + } + }; + }; + } + }, + + addAutocompletetoSirTrevorForm: function() { + $('[data-twitter-typeahead]', this.inner).spotlightSearchTypeAhead({bloodhound: this.bloodhound(), template: this.autocomplete_template()}).on('typeahead:selected typeahead:autocompleted', this.autocompletedHandler()).on( 'focus', function() { + if($(this).val() === '') { + $(this).data().ttTypeahead.input.trigger('queryChanged', ''); + } + }); + }, + + autocompletedHandler: function(e, data) { + var context = this; + + return function(e, data) { + $(this).typeahead("val", ""); + $(this).val(""); + + context.createItemPanel($.extend(data, {display: "true"})); + } + }, + + bloodhound: function() { + var block = this; + var results = new Bloodhound(Object.assign({ + datumTokenizer: function(d) { + return Bloodhound.tokenizers.whitespace(d.title); + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + limit: 100, + }, block.bloodhoundOptions())); + results.initialize(); + return results; + }, + }, + + + SirTrevor.Block.prototype.availableMixins.push("autocompleteable"); + })(jQuery); + + (function ($){ + SirTrevor.BlockMixins.Formable = { + mixinName: "Formable", + preload: true, + + initializeFormable: function() { + + if (this['afterLoadData'] === undefined) { + this['afterLoadData'] = function(data) { }; + } + }, + + formId: function(id) { + return this.blockID + "_" + id; + }, + + _serializeData: function() { + + var data = $(":input,textarea,select", this.inner).not(':input:radio').serializeJSON(); + + $(':input:radio:checked', this.inner).each(function(index, input) { + var key = $(input).data('key') || input.getAttribute('name'); + + if (!key.match("\\[")) { + data[key] = $(input).val(); + } + }); + + /* Simple to start. Add conditions later */ + if (this.hasTextBlock()) { + data.text = this.getTextBlockHTML(); + data.format = 'html'; + if (data.text && data.text.length > 0 && this.options.convertToMarkdown) { + data.text = stToMarkdown(data.text, this.type); + data.format = 'markdown'; + } + } + + return data; + }, + + loadData: function(data){ + if (this.hasTextBlock()) { + if (data.text && data.text.length > 0 && this.options.convertFromMarkdown && data.format !== "html") { + this.setTextBlockHTML(SirTrevor.toHTML(data.text, this.type)); + } else { + this.setTextBlockHTML(data.text); + } + } + this.loadFormDataByKey(data); + this.afterLoadData(data); + }, + + loadFormDataByKey: function(data) { + $(':input', this.inner).not('button,:input[type=hidden]').each(function(index, input) { + var key = $(input).data('key') || input.getAttribute('name'); + + if (key) { + + if (key.match("\\[\\]$")) { + key = key.replace("[]", ""); + } + + // by wrapping it in an array, this'll "just work" for radio and checkbox fields too + var input_data = data[key]; + + if (!(input_data instanceof Array)) { + input_data = [input_data]; + } + $(this).val(input_data); + } + }); + }, + }, + + + SirTrevor.Block.prototype.availableMixins.push("formable"); + })(jQuery); + + (function ($){ + SirTrevor.BlockMixins.Plustextable = { + mixinName: "Textable", + preload: true, + + initializeTextable: function() { + if (this['formId'] === undefined) { + this.withMixin(SirTrevor.BlockMixins.Formable); + } + + if (this['show_heading'] === undefined) { + this.show_heading = true; + } + }, + + align_key:"text-align", + text_key:"item-text", + heading_key: "title", + + text_area: function() { + return ` + <div class="row"> + <div class="col-md-8"> + <div class="form-group"> + ${this.heading()} + <div class="field"> + <label for="${this.formId(this.text_key)}" class="col-form-label">${i18n.t("blocks:textable:text")}</label> + <div id="${this.formId(this.text_key)}" class="st-text-block form-control" contenteditable="true"></div> + </div> + </div> + </div> + <div class="col-md-4"> + <div class="text-align"> + <p>${i18n.t("blocks:textable:align:title")}</p> + <input data-key="${this.align_key}" type="radio" name="${this.formId(this.align_key)}" id="${this.formId(this.align_key + "-left")}" value="left" checked="true"> + <label for="${this.formId(this.align_key + "-left")}">${i18n.t("blocks:textable:align:left")}</label> + <input data-key="${this.align_key}" type="radio" name="${this.formId(this.align_key)}" id="${this.formId(this.align_key + "-right")}" value="right"> + <label for="${this.formId(this.align_key + "-right")}">${i18n.t("blocks:textable:align:right")}</label> + </div> + </div> + </div>` + }, + + heading: function() { + if(this.show_heading) { + return `<div class="field"> + <label for="${this.formId(this.heading_key)}" class="col-form-label">${i18n.t("blocks:textable:heading")}</label> + <input type="text" class="form-control" id="${this.formId(this.heading_key)}" name="${this.heading_key}" /> + </div>` + } else { + return ""; + } + }, + }; + + + SirTrevor.Block.prototype.availableMixins.push("plustextable"); + })(jQuery); + + (function ($){ + Spotlight$1.Block = SirTrevor.Block.extend({ + scribeOptions: { + allowBlockElements: true, + tags: { p: true } + }, + formable: true, + editorHTML: function() { + return ''; + }, + beforeBlockRender: function() { + this.availableMixins.forEach(function(mixin) { + if (this[mixin] && SirTrevor.BlockMixins[this.capitalize(mixin)].preload) { + this.withMixin(SirTrevor.BlockMixins[this.capitalize(mixin)]); + } + }, this); + }, + $instance: function() { return $('#' + this.instanceID); }, + capitalize: function(string) { + return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); + } + }); + })(jQuery); + + Spotlight$1.Block.Resources = (function(){ + + return Spotlight$1.Block.extend({ + type: "resources", + formable: true, + autocompleteable: true, + show_heading: true, + + title: function() { return i18n.t("blocks:" + this.type + ":title"); }, + description: function() { return i18n.t("blocks:" + this.type + ":description"); }, + + icon_name: "resources", + blockGroup: function() { return i18n.t("blocks:group:items") }, + + primary_field_key: "primary-caption-field", + show_primary_field_key: "show-primary-caption", + secondary_field_key: "secondary-caption-field", + show_secondary_field_key: "show-secondary-caption", + + display_checkbox: "display-checkbox", + + globalIndex: 0, + + _itemPanelIiifFields: function(index, data) { + return []; + }, + + _itemPanel: function(data) { + var index = "item_" + this.globalIndex++; + var checked; + if (data.display == "true") { + checked = "checked='checked'"; + } else { + checked = ""; + } + var resource_id = data.slug || data.id; + var markup = ` + <li class="field form-inline dd-item dd3-item" data-resource-id="${resource_id}" data-id="${index}" id="${this.formId("item_" + data.id)}"> + <input type="hidden" name="item[${index}][id]" value="${resource_id}" /> + <input type="hidden" name="item[${index}][title]" value="${data.title}" /> + ${this._itemPanelIiifFields(index, data)} + <input data-property="weight" type="hidden" name="item[${index}][weight]" value="${data.weight}" /> + <div class="card d-flex dd3-content"> + <div class="dd-handle dd3-handle">${i18n.t("blocks:resources:panel:drag")}</div> + <div class="card-header item-grid"> + <div class="d-flex"> + <div class="checkbox"> + <input name="item[${index}][display]" type="hidden" value="false" /> + <input name="item[${index}][display]" id="${this.formId(this.display_checkbox + '_' + data.id)}" type="checkbox" ${checked} class="item-grid-checkbox" value="true" /> + <label class="sr-only" for="${this.formId(this.display_checkbox + '_' + data.id)}">${i18n.t("blocks:resources:panel:display")}</label> + </div> + <div class="pic"> + <img class="img-thumbnail" src="${(data.thumbnail_image_url || ((data.iiif_tilesource || "").replace("/info.json", "/full/!100,100/0/default.jpg")))}" /> + </div> + <div class="main"> + <div class="title card-title">${data.title}</div> + <div>${(data.slug || data.id)}</div> + </div> + <div class="remove float-right"> + <a data-item-grid-panel-remove="true" href="#">${i18n.t("blocks:resources:panel:remove")}</a> + </div> + </div> + <div data-panel-image-pagination="true"></div> + </div> + </div> + </li> + `; + + const panel = $(markup); + var context = this; + + $('.remove a', panel).on('click', function(e) { + e.preventDefault(); + $(this).closest('.field').remove(); + context.afterPanelDelete(); + + }); + + this.afterPanelRender(data, panel); + + return panel; + }, + + afterPanelRender: function(data, panel) { + + }, + + afterPanelDelete: function() { + + }, + + createItemPanel: function(data) { + var panel = this._itemPanel(data); + $(panel).appendTo($('.panels > ol', this.inner)); + $('[data-behavior="nestable"]', this.inner).trigger('change'); + }, + + item_options: function() { return ""; }, + + content: function() { + var templates = [this.items_selector()]; + if (this.plustextable) { + templates.push(this.text_area()); + } + return templates.join("<hr />\n"); + }, + + items_selector: function() { return [ + '<div class="row">', + '<div class="col-md-8">', + '<div class="form-group">', + '<div class="panels dd nestable-item-grid" data-behavior="nestable" data-max-depth="1"><ol class="dd-list"></ol></div>', + this.autocomplete_control(), + '</div>', + '</div>', + '<div class="col-md-4">', + this.item_options(), + '</div>', + '</div>'].join("\n") + }, + + editorHTML: function() { + return `<div class="form resources-admin clearfix"> + <div class="widget-header"> + ${this.description()} + </div> + ${this.content()} + </div>` + }, + + onBlockRender: function() { + Module.init($('[data-behavior="nestable"]', this.inner)); + + $('[data-input-select-target]', this.inner).selectRelatedInput(); + }, + + afterLoadData: function(data) { + var context = this; + $.each(Object.keys(data.item || {}).map(function(k) { return data.item[k]}).sort(function(a,b) { return a.weight - b.weight; }), function(index, item) { + context.createItemPanel(item); + }); + }, + }); + + })(); + + SirTrevor.Blocks.Browse = (function(){ + + return Spotlight$1.Block.Resources.extend({ + type: "browse", + + icon_name: "browse", + + autocomplete_url: function() { + return $(this.inner).closest('form[data-autocomplete-exhibit-searches-path]').data('autocomplete-exhibit-searches-path').replace("%25QUERY", "%QUERY"); + }, + autocomplete_template: function() { return '<div class="autocomplete-item{{#unless published}} blacklight-private{{/unless}}">{{#if thumbnail_image_url}}<div class="document-thumbnail"><img class="img-thumbnail" src="{{thumbnail_image_url}}" /></div>{{/if}}<span class="autocomplete-title">{{full_title}}</span><br/><small>&nbsp;&nbsp;{{description}}</small></div>' }, + + bloodhoundOptions: function() { + return { + prefetch: { + url: this.autocomplete_url(), + ttl: 0 + } + }; + }, + + _itemPanel: function(data) { + var index = "item_" + this.globalIndex++; + var checked; + if (data.display == "true") { + checked = "checked='checked'"; + } else { + checked = ""; + } + var resource_id = data.slug || data.id; + var markup = ` + <li class="field form-inline dd-item dd3-item" data-resource-id="${resource_id}" data-id="${index}" id="${this.formId("item_" + data.id)}"> + <input type="hidden" name="item[${index}][id]" value="${resource_id}" /> + <input type="hidden" name="item[${index}][full_title]" value="${(data.full_title || data.title)}" /> + <input data-property="weight" type="hidden" name="item[${index}][weight]" value="${data.weight}" /> + <div class="card d-flex dd3-content"> + <div class="dd-handle dd3-handle">${i18n.t("blocks:resources:panel:drag")}</div> + <div class="card-header item-grid"> + <div class="d-flex"> + <div class="checkbox"> + <input name="item[${index}][display]" type="hidden" value="false" /> + <input name="item[${index}][display]" id="${this.formId(this.display_checkbox + '_' + data.id)}" type="checkbox" ${checked} class="item-grid-checkbox" value="true" /> + <label class="sr-only" for="${this.formId(this.display_checkbox + '_' + data.id)}">${i18n.t("blocks:resources:panel:display")}</label> + </div> + <div class="pic"> + <img class="img-thumbnail" src="${(data.thumbnail_image_url || ((data.iiif_tilesource || "").replace("/info.json", "/full/!100,100/0/default.jpg")))}" /> + </div> + <div class="main"> + <div class="title card-title">${(data.full_title || data.title)}</div> + <div>${(data.slug || data.id)}</div> + </div> + <div class="remove float-right"> + <a data-item-grid-panel-remove="true" href="#">${i18n.t("blocks:resources:panel:remove")}</a> + </div> + </div> + </div> + </div> + </li>`; + + var panel = $(markup); + var context = this; + + $('.remove a', panel).on('click', function(e) { + e.preventDefault(); + $(this).closest('.field').remove(); + context.afterPanelDelete(); + + }); + + this.afterPanelRender(data, panel); + + return panel; + }, + + item_options: function() { return ` + <label> + <input type="hidden" name="display-item-counts" value="false" /> + <input type="checkbox" name="display-item-counts" value="true" checked /> + ${i18n.t("blocks:browse:item_counts")} + </label>` + }, + }); + + })(); + + /* + Sir Trevor BrowseGroupCategories + */ + + SirTrevor.Blocks.BrowseGroupCategories = (function(){ + + return Spotlight$1.Block.Resources.extend({ + type: "browse_group_categories", + icon_name: "browse", + bloodhoundOptions: function() { + var that = this; + return { + prefetch: { + url: this.autocomplete_url(), + ttl: 0, + filter: function(response) { + // Let the dom know that the response has been returned + $(that.inner).attr('data-browse-groups-fetched', true); + return response; + } + } + }; + }, + + autocomplete_control: function() { + return `<input type="text" class="st-input-string form-control item-input-field" data-twitter-typeahead="true" placeholder="${i18n.t("blocks:browse_group_categories:autocomplete")}"/>` + }, + autocomplete_template: function() { return '<div class="autocomplete-item{{#unless published}} blacklight-private{{/unless}}"><span class="autocomplete-title">{{title}}</span><br/></div>' }, + autocomplete_url: function() { return $(this.inner).closest('form[data-autocomplete-exhibit-browse-groups-path]').data('autocomplete-exhibit-browse-groups-path').replace("%25QUERY", "%QUERY"); }, + _itemPanel: function(data) { + var index = "item_" + this.globalIndex++; + var checked; + if (data.display == "true") { + checked = "checked='checked'"; + } else { + checked = ""; + } + var resource_id = data.slug || data.id; + var markup = ` + <li class="field form-inline dd-item dd3-item" data-resource-id="${resource_id}" data-id="${index}" id="${this.formId("item_" + data.id)}"> + <input type="hidden" name="item[${index}][id]" value="${resource_id}" /> + <input type="hidden" name="item[${index}][title]" value="${data.title}" /> + <input data-property="weight" type="hidden" name="item[${index}][weight]" value="${data.weight}" /> + <div class="card d-flex dd3-content"> + <div class="dd-handle dd3-handle">${i18n.t("blocks:resources:panel:drag")}</div> + <div class="d-flex card-header item-grid justify-content-between"> + <div class="d-flex flex-grow-1"> + <div class="checkbox"> + <input name="item[${index}][display]" type="hidden" value="false" /> + <input name="item[${index}][display]" id="${this.formId(this.display_checkbox + '_' + data.id)}" type="checkbox" ${checked} class="item-grid-checkbox" value="true" /> + <label class="sr-only" for="${this.formId(this.display_checkbox + '_' + data.id)}">${i18n.t("blocks:resources:panel:display")}</label> + </div> + <div class="main"> + <div class="title card-title">${data.title}</div> + </div> + </div> + <div class="d-flex"> + <a data-item-grid-panel-remove="true" href="#">${i18n.t("blocks:resources:panel:remove")}</a> + </div> + </div> + </div> + </li>`; + + const panel = $(markup); + var context = this; + + $('a[data-item-grid-panel-remove]', panel).on('click', function(e) { + e.preventDefault(); + $(this).closest('.field').remove(); + context.afterPanelDelete(); + + }); + + this.afterPanelRender(data, panel); + + return panel; + }, + + item_options: function() { return ` + '<label> + <input type="hidden" name="display-item-counts" value="false" /> + <input type="checkbox" name="display-item-counts" value="true" checked /> + ${i18n.t("blocks:browse_group_categories:item_counts")} + </label>` + }, + }); + })(); + + /* + Sir Trevor ItemText Block. + This block takes an ID, + fetches the record from solr, + displays the image, title, + and any provided text + and displays them. + */ + + SirTrevor.Blocks.Iframe = (function(){ + + return SirTrevor.Block.extend({ + type: "Iframe", + formable: true, + + title: function() { return i18n.t('blocks:iframe:title'); }, + description: function() { return i18n.t('blocks:iframe:description'); }, + + icon_name: "iframe", + + editorHTML: function() { + return `<div class="clearfix"> + <div class="widget-header"> + ${this.description()} + </div> + <textarea name="code" class="form-control" rows="5" placeholder="${i18n.t("blocks:iframe:placeholder")}"></textarea> + </div>`; + } + }); + })(); + + SirTrevor.Blocks.LinkToSearch = (function(){ + + return SirTrevor.Blocks.Browse.extend({ + + type: "link_to_search", + + icon_name: 'search_results', + + searches_key: "slug", + view_key: "view", + plustextable: false, + + }); + })(); + + /* + Sir Trevor ItemText Block. + This block takes an ID, + fetches the record from solr, + displays the image, title, + and any provided text + and displays them. + */ + + SirTrevor.Blocks.Oembed = (function(){ + + return Spotlight$1.Block.extend({ + plustextable: true, + + id_key:"url", + + type: "oembed", + + title: function() { return i18n.t('blocks:oembed:title'); }, + description: function() { return i18n.t('blocks:oembed:description'); }, + + icon_name: "oembed", + show_heading: false, + + editorHTML: function () { + return `<div class="form oembed-text-admin clearfix"> + <div class="widget-header"> + ${this.description()} + </div> + <div class="row"> + <div class="form-group col-md-8"> + <label for="${this.formId(id_key)}">${i18n.t("blocks:oembed:url")}</label> + <input name="${id_key}" class="form-control col-md-6" type="text" id="${this.formId(id_key)}" /> + </div> + </div> + ${this.text_area()} + </div>` + } + }); + })(); + + SirTrevor.Blocks.FeaturedPages = (function(){ + + return Spotlight$1.Block.Resources.extend({ + type: "featured_pages", + + icon_name: "pages", + + autocomplete_url: function() { return $(this.inner).closest('form[data-autocomplete-exhibit-pages-path]').data('autocomplete-exhibit-pages-path').replace("%25QUERY", "%QUERY"); }, + autocomplete_template: function() { return '<div class="autocomplete-item{{#unless published}} blacklight-private{{/unless}}">{{#if thumbnail_image_url}}<div class="document-thumbnail"><img class="img-thumbnail" src="{{thumbnail_image_url}}" /></div>{{/if}}<span class="autocomplete-title">{{title}}</span><br/><small>&nbsp;&nbsp;{{description}}</small></div>' }, + bloodhoundOptions: function() { + return { + prefetch: { + url: this.autocomplete_url(), + ttl: 0 + } + }; + } + }); + + })(); + + /* + Sir Trevor ItemText Block. + This block takes an ID, + fetches the record from solr, + displays the image, title, + and any provided text + and displays them. + */ + + SirTrevor.Blocks.Rule = (function(){ + + return SirTrevor.Block.extend({ + type: "rule", + + title: function() { return i18n.t('blocks:rule:title'); }, + + icon_name: "rule", + + editorHTML: function() { + return '<hr />' + } + }); + })(); + + //= require spotlight/admin/blocks/browse_block + + SirTrevor.Blocks.SearchResults = (function(){ + + return SirTrevor.Blocks.Browse.extend({ + + type: "search_results", + + icon_name: 'search_results', + + searches_key: "slug", + view_key: "view", + plustextable: false, + + content: function() { + return this.items_selector() + }, + + item_options: function() { + var block = this; + var fields = $('[data-blacklight-configuration-search-views]').data('blacklight-configuration-search-views'); + + return $.map(fields, function(field) { + return `<div> + <label for='${block.formId(block.view_key + field.key)}'> + <input id='${block.formId(block.view_key + field.key)}' name='${block.view_key}[]' type='checkbox' value='${field.key}' /> + ${field.label} + </label> + </div>` + }).join("\n"); + }, + + afterPanelRender: function(data, panel) { + $(this.inner).find('.item-input-field').attr("disabled", "disabled"); + }, + + afterPanelDelete: function() { + $(this.inner).find('.item-input-field').removeAttr("disabled"); + }, + + }); + })(); + + SirTrevor.Blocks.SolrDocumentsBase = (function(){ + + return Spotlight$1.Block.Resources.extend({ + plustextable: true, + autocomplete_url: function() { return this.$instance().closest('form[data-autocomplete-exhibit-catalog-path]').data('autocomplete-exhibit-catalog-path').replace("%25QUERY", "%QUERY"); }, + autocomplete_template: function() { return '<div class="autocomplete-item{{#if private}} blacklight-private{{/if}}">{{#if thumbnail}}<div class="document-thumbnail"><img class="img-thumbnail" src="{{thumbnail}}" /></div>{{/if}}<span class="autocomplete-title">{{title}}</span><br/><small>&nbsp;&nbsp;{{description}}</small></div>' }, + + transform_autocomplete_results: function(response) { + return $.map(response['docs'], function(doc) { + return doc; + }) + }, + + caption_option_values: function() { + var fields = $('[data-blacklight-configuration-index-fields]').data('blacklight-configuration-index-fields'); + + return $.map(fields, function(field) { + return $('<option />').val(field.key).text(field.label)[0].outerHTML; + }).join("\n"); + }, + + item_options: function() { return this.caption_options(); }, + + caption_options: function() { return ` + <div class="field-select primary-caption" data-behavior="item-caption-admin"> + <input name="${this.show_primary_field_key}" type="hidden" value="false" /> + <input data-input-select-target="#${this.formId(this.primary_field_key)}" name="${this.show_primary_field_key}" id="${this.formId(this.show_primary_field_key)}" type="checkbox" value="true" /> + <label for="${this.formId(this.show_primary_field_key)}">${i18n.t("blocks:solr_documents:caption:primary")}</label> + <select data-input-select-target="#${this.formId(this.show_primary_field_key)}" name="${this.primary_field_key}" id="${this.formId(this.primary_field_key)}"> + <option value="">${i18n.t("blocks:solr_documents:caption:placeholder")}</option> + ${this.caption_option_values()} + </select> + </div> + <div class="field-select secondary-caption" data-behavior="item-caption-admin"> + <input name="${this.show_secondary_field_key}" type="hidden" value="false" /> + <input data-input-select-target="#${this.formId(this.secondary_field_key)}" name="${this.show_secondary_field_key}" id="${this.formId(this.show_secondary_field_key)}" type="checkbox" value="true" /> + <label for="${this.formId(this.show_secondary_field_key)}">${i18n.t("blocks:solr_documents:caption:secondary")}</label> + <select data-input-select-target="#${this.formId(this.show_secondary_field_key)}" name="${this.secondary_field_key}" id="${this.formId(this.secondary_field_key)}"> + <option value="">${i18n.t("blocks:solr_documents:caption:placeholder")}</option> + ${this.caption_option_values()} + </select> + </div> + `}, + + _itemPanelIiifFields: function(index, data) { + return [ + // '<input type="hidden" name="item[' + index + '][iiif_region]" value="' + (data.iiif_region) + '"/>', + // for legacy compatiblity: + '<input type="hidden" name="item[' + index + '][thumbnail_image_url]" value="' + (data.thumbnail_image_url || data.thumbnail || "") + '"/>', + '<input type="hidden" name="item[' + index + '][full_image_url]" value="' + (data.full_image_url || data.thumbnail_image_url || data.thumbnail || "") + '"/>', + '<input type="hidden" name="item[' + index + '][iiif_tilesource]" value="' + (data.iiif_tilesource) + '"/>', + '<input type="hidden" name="item[' + index + '][iiif_manifest_url]" value="' + (data.iiif_manifest_url) + '"/>', + '<input type="hidden" name="item[' + index + '][iiif_canvas_id]" value="' + (data.iiif_canvas_id) + '"/>', + '<input type="hidden" name="item[' + index + '][iiif_image_id]" value="' + (data.iiif_image_id) + '"/>', + ].join("\n"); + }, + setIiifFields: function(panel, data, initialize) { + var legacyThumbnailField = $(panel).find('[name$="[thumbnail_image_url]"]'); + var legacyFullField = $(panel).find('[name$="[full_image_url]"]'); + + if (initialize && legacyThumbnailField.val().length > 0) { + return; + } + + legacyThumbnailField.val(""); + legacyFullField.val(""); + $(panel).find('[name$="[iiif_image_id]"]').val(data.imageId); + $(panel).find('[name$="[iiif_tilesource]"]').val(data.tilesource); + $(panel).find('[name$="[iiif_manifest_url]"]').val(data.manifest); + $(panel).find('[name$="[iiif_canvas_id]"]').val(data.canvasId); + $(panel).find('img.img-thumbnail').attr('src', data.thumbnail_image_url || data.tilesource.replace("/info.json", "/full/100,100/0/default.jpg")); + }, + afterPanelRender: function(data, panel) { + var context = this; + var manifestUrl = data.iiif_manifest || data.iiif_manifest_url; + + if (!manifestUrl) { + $(panel).find('[name$="[thumbnail_image_url]"]').val(data.thumbnail_image_url || data.thumbnail); + $(panel).find('[name$="[full_image_url]"]').val(data.full_image_url); + + return; + } + + $.ajax(manifestUrl).done( + function(manifest) { + var iiifManifest = new Iiif(manifestUrl, manifest); + + var thumbs = iiifManifest.imagesArray(); + + if (!data.iiif_image_id) { + context.setIiifFields(panel, thumbs[0], !!data.iiif_manifest_url); + } + + + if(thumbs.length > 1) { + panel.multiImageSelector(thumbs, function(selectorImage) { + context.setIiifFields(panel, selectorImage, false); + }, data.iiif_image_id); + } + } + ); + } + }); + + })(); + + //= require spotlight/admin/blocks/solr_documents_base_block + + SirTrevor.Blocks.SolrDocuments = (function(){ + + return SirTrevor.Blocks.SolrDocumentsBase.extend({ + type: "solr_documents", + + icon_name: "items", + + item_options: function() { return this.caption_options() + this.zpr_option(); }, + + zpr_option: function() { + return ` + <div> + <input name="${this.zpr_key}" type="hidden" value="false" /> + <input name="${this.zpr_key}" id="${this.formId(this.zpr_key)}" data-key="${this.zpr_key}" type="checkbox" value="true" /> + <label for="${this.formId(this.zpr_key)}">${i18n.t("blocks:solr_documents:zpr:title")}</label> + </div> + ` + }, + + zpr_key: 'zpr_link' + }); + + })(); + + //= require spotlight/admin/blocks/solr_documents_base_block + + SirTrevor.Blocks.SolrDocumentsCarousel = (function(){ + + return SirTrevor.Blocks.SolrDocumentsBase.extend({ + plustextable: false, + type: "solr_documents_carousel", + + icon_name: "item_carousel", + + auto_play_images_key: "auto-play-images", + auto_play_images_interval_key: "auto-play-images-interval", + max_height_key: "max-height", + + item_options: function() { return "" }, + + carouselCycleTimesInSeconds: { + values: [ 3, 5, 8, 12, 20 ], + selected: 5 + }, + + carouselMaxHeights: { + values: { 'Small': 'small', 'Medium': 'medium', 'Large': 'large' }, + selected: 'Medium' + }, + + item_options: function() { + return `${this.caption_options()} + <div class="field-select auto-cycle-images" data-behavior="auto-cycle-images"> + <input name="${this.auto_play_images_key}" type="hidden" value="false" /> + <input name="${this.auto_play_images_key}" id="${this.formId(this.auto_play_images_key)}" data-key="${this.auto_play_images_key}" type="checkbox" value="true" checked/> + <label for="${this.formId(this.auto_play_images_key)}">${i18n.t("blocks:solr_documents_carousel:interval:title")}</label> + <select name="${this.auto_play_images_interval_key}" id="${this.formId(this.auto_play_images_interval_key)}" data=key="${this.auto_play_images_interval_key}"> + <option value="">${i18n.t("blocks:solr_documents_carousel:interval:placeholder")}</option> + ${this.addCarouselCycleOptions(this.carouselCycleTimesInSeconds)} + </select> + </div> + <div class="field-select max-heights" data-behavior="max-heights"> + <label for="${this.formId(this.max_height_key)}">${i18n.t("blocks:solr_documents_carousel:height:title")}</label><br/> + ${this.addCarouselMaxHeightOptions(this.carouselMaxHeights)} + </div>` + }, + + addCarouselCycleOptions: function(options) { + var html = ''; + + $.each(options.values, function(index, interval) { + var selected = (interval === options.selected) ? 'selected' : '', + intervalInMilliSeconds = parseInt(interval, 10) * 1000; + + html += '<option value="' + intervalInMilliSeconds + '" ' + selected + '>' + interval + ' seconds</option>'; + }); + + return html; + }, + + addCarouselMaxHeightOptions: function(options) { + var html = '', + _this = this; + + $.each(options.values, function(size, px) { + var checked = (size === options.selected) ? 'checked' : '', + id = _this.formId(_this.max_height_key); + + html += '<input data-key="' + _this.max_height_key + '" type="radio" name="' + id + '" value="' + px + '" id="' + id + '" ' + checked + '>'; + html += '<label class="carousel-size" for="' + id + '">' + size + '</label>'; + }); + + return html; + }, + + afterPreviewLoad: function(options) { + $(this.inner).find('.carousel').carousel(); + + // the bootstrap carousel only initializes data-slide widgets on page load, so we need + // to initialize them ourselves.. + var clickHandler = function (e) { + var href; + var $this = $(this); + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')); // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()); + var slideIndex = $this.attr('data-slide-to'); + if (slideIndex) options.interval = false; + + $.fn.carousel.call($target, options); + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex); + } + + e.preventDefault(); + }; + + $(this.inner).find('.carousel') + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler); + } + + }); + + })(); + + //= require spotlight/admin/blocks/solr_documents_base_block + + SirTrevor.Blocks.SolrDocumentsEmbed = (function(){ + + return SirTrevor.Blocks.SolrDocumentsBase.extend({ + type: "solr_documents_embed", + + icon_name: "item_embed", + + item_options: function() { return "" }, + + afterPreviewLoad: function(options) { + $(this.inner).find('picture[data-openseadragon]').openseadragon(); + } + }); + + })(); + + //= require spotlight/admin/blocks/solr_documents_base_block + + SirTrevor.Blocks.SolrDocumentsFeatures = (function(){ + + return SirTrevor.Blocks.SolrDocumentsBase.extend({ + plustextable: false, + type: "solr_documents_features", + + icon_name: "item_features", + + afterPreviewLoad: function(options) { + $(this.inner).find('.carousel').carousel(); + + // the bootstrap carousel only initializes data-slide widgets on page load, so we need + // to initialize them ourselves.. + var clickHandler = function (e) { + var href; + var $this = $(this); + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')); // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()); + var slideIndex = $this.attr('data-slide-to'); + if (slideIndex) options.interval = false; + + $.fn.carousel.call($target, options); + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex); + } + + e.preventDefault(); + }; + + $(this.inner).find('.carousel') + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler); + } + + }); + + })(); + + //= require spotlight/admin/blocks/solr_documents_base_block + + SirTrevor.Blocks.SolrDocumentsGrid = (function(){ + + return SirTrevor.Blocks.SolrDocumentsBase.extend({ + type: "solr_documents_grid", + + icon_name: "item_grid", + + + item_options: function() { return "" } + }); + + })(); + + SirTrevor.Blocks.UploadedItems = (function(){ + return Spotlight$1.Block.Resources.extend({ + plustextable: true, + uploadable: true, + autocompleteable: false, + + id_key: 'file', + + type: 'uploaded_items', + + icon_name: 'items', + + blockGroup: 'undefined', + + // Clear out the default Uploadable upload options + // since we will be using our own custom controls + upload_options: { html: '' }, + + fileInput: function() { return $(this.inner).find('input[type="file"]'); }, + + onBlockRender: function(){ + Module.init($(this.inner).find('[data-behavior="nestable"]')); + + this.fileInput().on('change', (function(ev) { + this.onDrop(ev.currentTarget); + }).bind(this)); + }, + + onDrop: function(transferData){ + var file = transferData.files[0]; + (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null; + + // Handle one upload at a time + if (/image/.test(file.type)) { + this.loading(); + + this.uploader( + file, + function(data) { + this.createItemPanel(data); + this.fileInput().val(''); + this.ready(); + }, + function(error) { + this.addMessage(i18n.t('blocks:image:upload_error')); + this.ready(); + } + ); + } + }, + + title: function() { return i18n.t('blocks:uploaded_items:title'); }, + description: function() { return i18n.t('blocks:uploaded_items:description'); }, + + globalIndex: 0, + + _itemPanel: function(data) { + var index = "file_" + this.globalIndex++; + var checked = 'checked="checked"'; + + if (data.display == 'false') { + checked = ''; + } + + var dataId = data.id || data.uid; + var dataTitle = data.title || data.name; + var dataUrl = data.url || data.file.url; + + var markup = ` + <li class="field form-inline dd-item dd3-item" data-id="${index}" id="${this.formId("item_" + dataId)}"> + <input type="hidden" name="item[${index}][id]" value="${dataId}" /> + <input type="hidden" name="item[${index}][title]" value="${dataTitle}" /> + <input type="hidden" name="item[${index}][url]" data-item-grid-thumbnail="true" value="${dataUrl}"/> + <input data-property="weight" type="hidden" name="item[${index}][weight]" value="${data.weight}" /> + <div class="card d-flex dd3-content"> + <div class="dd-handle dd3-handle">${i18n.t("blocks:resources:panel:drag")}</div> + <div class="card-header d-flex item-grid"> + <div class="checkbox"> + <input name="item[${index}][display]" type="hidden" value="false" /> + <input name="item[${index}][display]" id="${this.formId(this.display_checkbox + '_' + dataId)}" type="checkbox" ${checked} class="item-grid-checkbox" value="true" /> + <label class="sr-only" for="${this.formId(this.display_checkbox + '_' + dataId)}">${i18n.t("blocks:resources:panel:display")}</label> + </div> + <div class="pic"> + <img class="img-thumbnail" src="${dataUrl}" /> + </div> + <div class="main form-horizontal"> + <div class="title card-title">${dataTitle}</div> + <div class="field row mr-3"> + <label for="${this.formId('caption_' + dataId)}" class="col-form-label col-md-3">${i18n.t("blocks:uploaded_items:caption")}</label> + <input type="text" class="form-control col" id="${this.formId('caption_' + dataId)}" name="item[${index}][caption]" data-field="caption"/> + </div> + <div class="field row mr-3"> + <label for="${this.formId('link_' + dataId)}" class="col-form-label col-md-3">${i18n.t("blocks:uploaded_items:link")}</label> + <input type="text" class="form-control col" id="${this.formId('link_' + dataId)}" name="item[${index}][link]" data-field="link"/> + </div> + </div> + <div class="remove float-right"> + <a data-item-grid-panel-remove="true" href="#">${i18n.t("blocks:resources:panel:remove")}</a> + </div> + </div> + </li>`; + + const panel = $(markup); + panel.find('[data-field="caption"]').val(data.caption); + panel.find('[data-field="link"]').val(data.link); + var context = this; + + $('.remove a', panel).on('click', function(e) { + e.preventDefault(); + $(this).closest('.field').remove(); + context.afterPanelDelete(); + }); + + this.afterPanelRender(data, panel); + + return panel; + }, + + editorHTML: function() { + return `<div class="form oembed-text-admin clearfix"> + <div class="widget-header"> + ${this.description()} + </div> + <div class="row"> + <div class="form-group col-md-8"> + <div class="panels dd nestable-item-grid" data-behavior="nestable" data-max-depth="1"> + <ol class="dd-list"> + </ol> + </div>, + <input type="file" id="uploaded_item_url" name="file[file_0][file_data]" /> + </div>' + <div class="col-md-4"> + <input name="${this.zpr_key}" type="hidden" value="false" /> + <input name="${this.zpr_key}" id="${this.formId(this.zpr_key)}" data-key=${this.zpr_key}" type="checkbox" value="true" /> + <label for="${this.formId(this.zpr_key)}">${ i18n.t("blocks:solr_documents:zpr:title")}</label> + </div> + </div> + ${this.text_area()} + </div>` + }, + + zpr_key: 'zpr_link' + }); + })(); + + (function() { + var BLOCK_REPLACER_CONTROL_TEMPLATE = function(block) { + var el = document.createElement('button'); + el.className = "st-block-controls__button"; + el.setAttribute('data-type', block.type); + el.type = "button"; + + var img = document.createElement('svg'); + img.className = "st-icon"; + img.setAttribute('role', 'img'); + + var use = document.createElement('use'); + use.setAttributeNS('https://www.w3.org/1999/xlink', 'href', SirTrevor.config.defaults.iconUrl + "#" + block.icon_name); + img.appendChild(use); + el.appendChild(img); + el.appendChild(document.createTextNode(block.title())); + + return el.outerHTML; + }; + + function generateBlocksHTML(Blocks, availableTypes) { + var groups = {}; + for(var i in availableTypes) { + var type = availableTypes[i]; + if (Blocks.hasOwnProperty(type) && Blocks[type].prototype.toolbarEnabled) { + var blockGroup; + + if ($.isFunction(Blocks[type].prototype.blockGroup)) { + blockGroup = Blocks[type].prototype.blockGroup(); + } else { + blockGroup = Blocks[type].prototype.blockGroup; + } + + if (blockGroup == 'undefined' || blockGroup === undefined) { + blockGroup = i18n.t("blocks:group:undefined"); + } + + groups[blockGroup] = groups[blockGroup] || []; + groups[blockGroup].push(BLOCK_REPLACER_CONTROL_TEMPLATE(Blocks[type].prototype)); + } + } + + function generateBlock(groups, key) { + var group = groups[key]; + var groupEl = $("<div class='st-controls-group'><div class='st-group-col-form-label'>" + key + "</div></div>"); + var buttons = group.reduce(function(memo, btn) { + return memo += btn; + }, ""); + groupEl.append(buttons); + return groupEl[0].outerHTML; + } + + var standardWidgets = generateBlock(groups, i18n.t("blocks:group:undefined")); + + var exhibitWidgets = Object.keys(groups).map(function(key) { + if (key !== i18n.t("blocks:group:undefined")) { + return generateBlock(groups, key); + } + }).filter(function (element) { + return element != null; + }); + + var blocks = [standardWidgets].concat(exhibitWidgets).join("<hr />"); + return blocks; + } + + function render(Blocks, availableTypes) { + var el = document.createElement('div'); + el.className = "st-block-controls__buttons"; + el.innerHTML = generateBlocksHTML.apply(null, arguments); + + var elButtons = document.createElement('div'); + elButtons.className = "spotlight-block-controls"; + elButtons.appendChild(el); + return elButtons; + } + + Spotlight$1.BlockControls = function() { }; + Spotlight$1.BlockControls.create = function(editor) { + // REFACTOR - should probably not know about blockManager + var el = render(SirTrevor.Blocks, editor.blockManager.blockTypes); + + function hide() { + var parent = el.parentNode; + if (!parent) { return; } + parent.removeChild(el); + parent.classList.remove("st-block--controls-active"); + return parent; + } + + function destroy() { + SirTrevor = null; + el = null; + } + + function insert(e) { + e.stopPropagation(); + + var parent = this.parentNode; + if (!parent || hide() === parent) { return; } + $('.st-block__inner', parent).after(el); + parent.classList.add("st-block--controls-active"); + } + + $(editor.wrapper).delegate(".st-block-replacer", "click", insert); + $(editor.wrapper).delegate(".st-block-controls__button", "click", insert); + + return { + el: el, + hide: hide, + destroy: destroy + }; + }; + })(); + + Spotlight$1.BlockLimits = function(editor) { + this.editor = editor; + }; + + Spotlight$1.BlockLimits.prototype.enforceLimits = function(editor) { + this.addEditorCallbacks(editor); + this.checkGlobalBlockTypeLimit()(); + }; + + Spotlight$1.BlockLimits.prototype.addEditorCallbacks = function(editor) { + SirTrevor.EventBus.on('block:create:new', this.checkBlockTypeLimitOnAdd()); + SirTrevor.EventBus.on('block:remove', this.checkGlobalBlockTypeLimit()); + }; + + Spotlight$1.BlockLimits.prototype.checkBlockTypeLimitOnAdd = function() { + var editor = this.editor; + + return function(block) { + var control = $(".st-block-controls__button[data-type='" + block.type + "']", editor.blockControls.el); + + control.prop("disabled", !editor.blockManager.canCreateBlock(block.class())); + }; + }; + + Spotlight$1.BlockLimits.prototype.checkGlobalBlockTypeLimit = function() { + // we don't know what type of block was created or removed.. So, try them all. + var editor = this.editor; + + return function() { + $.each(editor.blockManager.blockTypes, function(i, type) { + var block_type = SirTrevor.Blocks[type].prototype; + + var control = $(editor.blockControls.el).find(".st-block-controls__button[data-type='" + block_type.type + "']"); + control.prop("disabled", !editor.blockManager.canCreateBlock(type)); + }); + }; + }; + + SirTrevor.Locales.en.blocks = $.extend(SirTrevor.Locales.en.blocks, { + autocompleteable: { + placeholder: "Enter a title..." + }, + + browse: { + title: "Browse Categories", + description: "This widget highlights browse categories. Each highlighted category links to the corresponding browse category results page.", + item_counts: "Include item counts?" + }, + + browse_group_categories: { + autocomplete: "Enter a browse group title...", + title: "Browse Group Categories", + description: "This widget displays all browse categories associated with a selected browse group as a horizontally-scrolling row. Each selected browse group is displayed as a separate row. Each displayed category in a group links to the corresponding browse category results page.", + item_counts: "Include category item counts?" + }, + + link_to_search: { + title: "Saved Searches", + description: "This widget highlights saved searches. Each highlighted saved search links to the search results page generated by the saved search parameters. Any saved search listed on the Curation > Browse categories page, whether published or not, can be highlighted as a saved search.", + item_counts: "Include item counts?" + }, + + iframe: { + title: "IFrame", + description: "This widget embeds iframe-based embed code into pages", + placeholder: "Enter embed code here. It should begin with e.g. '<iframe'" + }, + + oembed: { + title: "Embed + Text", + description: "This widget embeds an oEmbed-supported web resource and a text block to the left or right of it. Examples of oEmbed-supported resources include those from YouTube, Twitter, Flickr, and SlideShare.", + url: "URL", + }, + + uploaded_items: { + title: "Uploaded Item Row", + description: "This widget displays uploaded items in a horizontal row. Optionally, you can add a heading and/or text to be displayed adjacent to the items. The item caption and link URL fields are also optional.", + caption: 'Caption', + link: 'Link URL' + }, + + featured_pages: { + title: "Pages", + description: "This widget highlights pages from this exhibit. Each highlighted item links to the corresponding page." + }, + + resources: { + panel: { + drag: "Drag", + display: "Display?", + remove: "Remove" + } + }, + + rule: { + title: "Horizontal Rule" + }, + + search_results: { + title: "Search Results", + description: "This widget displays a set of search results on a page. Specify a search result set by selecting an existing browse category. You can also select the view types that are available to the user when viewing the result set." + }, + + solr_documents: { + title: "Item Row", + description: "This widget displays exhibit items in a horizontal row. Optionally, you can add a heading and/or text to be displayed adjacent to the items.", + caption: { + placeholder: "Select...", + primary: "Primary caption", + secondary: "Secondary caption" + }, + zpr: { + title: 'Offer "View larger" option' + } + }, + + solr_documents_carousel: { + title: "Item Carousel", + description: "This widget displays exhibit items in a carousel. You can configure the item captions, how the images are cycled, and the size of the carousel.", + interval: { + title: "Automatically cycle images", + placeholder: "Select..." + }, + height: { + title: "Maximum carousel height" + } + }, + + solr_documents_embed: { + title: "Item Embed", + description: "This widget embeds an exhibit item in a viewer on a page. Optionally, you can add a heading to be displayed above the viewer and/or text to be displayed adjacent to the viewer.", + }, + + solr_documents_features: { + title: "Item Slideshow", + description: "This widget displays exhibit items in a static slideshow. The user will move between items in the slideshow using the field you select as the primary caption." + }, + + solr_documents_grid: { + title: "Item Grid", + description: "This widget displays exhibit items in a multi-row grid. Optionally, you can add a heading and/or text to be displayed adjacent to the items." + }, + + textable: { + heading: "Heading", + text: "Text", + align: { + title: "Display text on:", + left: "Left", + right: "Right" + } + }, + + group: { + undefined: "Standard widgets", + items: "Exhibit item widgets" + } + }); + + // import '../../../../vendor/assets/javascripts/sir-trevor' + + + class AdminIndex { + connect() { + new AddAnother().connect(); + new AddNewButton().connect(); + new Appearance().connect(); + new CopyEmailAddress().connect(); + new Croppable().connect(); + new EditInPlace().connect(); + new ExhibitTagAutocomplete().connect(); + new Exhibits().connect(); + new FormObserver().connect(); + new Locks().connect(); + new BlacklightConfiguration().connect(); + new Pages().connect(); + new ProgressMonitor().connect(); + new ReadonlyCheckbox().connect(); + new SelectRelatedInput().connect(); + new Tabs().connect(); + new TranslationProgress().connect(); + new Users().connect(); + addAutocompletetoFeaturedImage(); + Module.init(); + } + } + + Spotlight$1.onLoad(() => { + new UserIndex().connect(); + new AdminIndex().connect(); + }); + + return Spotlight$1; + +})); +//# sourceMappingURL=spotlight.js.map